Path: blob/main/src/resources/extensions/quarto/confluence/luaunit.lua
12923 views
--[[1luaunit.lua23Description: A unit testing framework4Homepage: https://github.com/bluebird75/luaunit5Development by Philippe Fremy <[email protected]>6Based on initial work of Ryu, Gwang (http://www.gpgstudy.com/gpgiki/LuaUnit)7License: BSD License, see LICENSE.txt8]]--910require("math")11local M = {}1213-- private exported functions (for testing)14M.private = {}1516M.VERSION = '3.4'17M._VERSION = M.VERSION -- For LuaUnit v2 compatibility1819-- a version which distinguish between regular Lua and LuaJit20M._LUAVERSION = (jit and jit.version) or _VERSION2122--[[ Some people like assertEquals( actual, expected ) and some people prefer23assertEquals( expected, actual ).24]]--25M.ORDER_ACTUAL_EXPECTED = true26M.PRINT_TABLE_REF_IN_ERROR_MSG = false27M.LINE_LENGTH = 8028M.TABLE_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items29M.LIST_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items3031-- this setting allow to remove entries from the stack-trace, for32-- example to hide a call to a framework which would be calling luaunit33M.STRIP_EXTRA_ENTRIES_IN_STACK_TRACE = 03435--[[ EPS is meant to help with Lua's floating point math in simple corner36cases like almostEquals(1.1-0.1, 1), which may not work as-is (e.g. on numbers37with rational binary representation) if the user doesn't provide some explicit38error margin.3940The default margin used by almostEquals() in such cases is EPS; and since41Lua may be compiled with different numeric precisions (single vs. double), we42try to select a useful default for it dynamically. Note: If the initial value43is not acceptable, it can be changed by the user to better suit specific needs.4445See also: https://en.wikipedia.org/wiki/Machine_epsilon46]]47M.EPS = 2 ^ -52 -- = machine epsilon for "double", ~2.22E-1648if math.abs(1.1 - 1 - 0.1) > M.EPS then49-- rounding error is above EPS, assume single precision50M.EPS = 2 ^ -23 -- = machine epsilon for "float", ~1.19E-0751end5253-- set this to false to debug luaunit54local STRIP_LUAUNIT_FROM_STACKTRACE = true5556M.VERBOSITY_DEFAULT = 1057M.VERBOSITY_LOW = 158M.VERBOSITY_QUIET = 059M.VERBOSITY_VERBOSE = 2060M.DEFAULT_DEEP_ANALYSIS = nil61M.FORCE_DEEP_ANALYSIS = true62M.DISABLE_DEEP_ANALYSIS = false6364-- set EXPORT_ASSERT_TO_GLOBALS to have all asserts visible as global values65-- EXPORT_ASSERT_TO_GLOBALS = true6667-- we need to keep a copy of the script args before it is overriden68local cmdline_argv = rawget(_G, "arg")6970M.FAILURE_PREFIX = 'LuaUnit test FAILURE: ' -- prefix string for failed tests71M.SUCCESS_PREFIX = 'LuaUnit test SUCCESS: ' -- prefix string for successful tests finished early72M.SKIP_PREFIX = 'LuaUnit test SKIP: ' -- prefix string for skipped tests73747576M.USAGE = [[Usage: lua <your_test_suite.lua> [options] [testname1 [testname2] ... ]77Options:78-h, --help: Print this help79--version: Print version information80-v, --verbose: Increase verbosity81-q, --quiet: Set verbosity to minimum82-e, --error: Stop on first error83-f, --failure: Stop on first failure or error84-s, --shuffle: Shuffle tests before running them85-o, --output OUTPUT: Set output type to OUTPUT86Possible values: text, tap, junit, nil87-n, --name NAME: For junit only, mandatory name of xml file88-r, --repeat NUM: Execute all tests NUM times, e.g. to trig the JIT89-p, --pattern PATTERN: Execute all test names matching the Lua PATTERN90May be repeated to include several patterns91Make sure you escape magic chars like +? with %92-x, --exclude PATTERN: Exclude all test names matching the Lua PATTERN93May be repeated to exclude several patterns94Make sure you escape magic chars like +? with %95testname1, testname2, ... : tests to run in the form of testFunction,96TestClass or TestClass.testMethod9798You may also control LuaUnit options with the following environment variables:99* LUAUNIT_OUTPUT: same as --output100* LUAUNIT_JUNIT_FNAME: same as --name ]]101102----------------------------------------------------------------103--104-- general utility functions105--106----------------------------------------------------------------107108--[[ Note on catching exit109110I have seen the case where running a big suite of test cases and one of them would111perform a os.exit(0), making the outside world think that the full test suite was executed112successfully.113114This is an attempt to mitigate this problem: we override os.exit() to now let a test115exit the framework while we are running. When we are not running, it behaves normally.116]]117118M.oldOsExit = os.exit119os.exit = function(...)120if M.LuaUnit and #M.LuaUnit.instances ~= 0 then121local msg = [[You are trying to exit but there is still a running instance of LuaUnit.122LuaUnit expects to run until the end before exiting with a complete status of successful/failed tests.123124To force exit LuaUnit while running, please call before os.exit (assuming lu is the luaunit module loaded):125126lu.unregisterCurrentSuite()127128]]129M.private.error_fmt(2, msg)130end131M.oldOsExit(...)132end133134local function pcall_or_abort(func, ...)135-- unpack is a global function for Lua 5.1, otherwise use table.unpack136local unpack = rawget(_G, "unpack") or table.unpack137local result = { pcall(func, ...) }138if not result[1] then139-- an error occurred140print(result[2]) -- error message141print()142print(M.USAGE)143os.exit(-1)144end145return unpack(result, 2)146end147148local crossTypeOrdering = {149number = 1, boolean = 2, string = 3, table = 4, other = 5150}151local crossTypeComparison = {152number = function(a, b)153return a < b154end,155string = function(a, b)156return a < b157end,158other = function(a, b)159return tostring(a) < tostring(b)160end,161}162163local function crossTypeSort(a, b)164local type_a, type_b = type(a), type(b)165if type_a == type_b then166local func = crossTypeComparison[type_a] or crossTypeComparison.other167return func(a, b)168end169type_a = crossTypeOrdering[type_a] or crossTypeOrdering.other170type_b = crossTypeOrdering[type_b] or crossTypeOrdering.other171return type_a < type_b172end173174local function __genSortedIndex(t)175-- Returns a sequence consisting of t's keys, sorted.176local sortedIndex = {}177178for key, _ in pairs(t) do179table.insert(sortedIndex, key)180end181182table.sort(sortedIndex, crossTypeSort)183return sortedIndex184end185M.private.__genSortedIndex = __genSortedIndex186187local function sortedNext(state, control)188-- Equivalent of the next() function of table iteration, but returns the189-- keys in sorted order (see __genSortedIndex and crossTypeSort).190-- The state is a temporary variable during iteration and contains the191-- sorted key table (state.sortedIdx). It also stores the last index (into192-- the keys) used by the iteration, to find the next one quickly.193local key194195--print("sortedNext: control = "..tostring(control) )196if control == nil then197-- start of iteration198state.count = #state.sortedIdx199state.lastIdx = 1200key = state.sortedIdx[1]201return key, state.t[key]202end203204-- normally, we expect the control variable to match the last key used205if control ~= state.sortedIdx[state.lastIdx] then206-- strange, we have to find the next value by ourselves207-- the key table is sorted in crossTypeSort() order! -> use bisection208local lower, upper = 1, state.count209repeat210state.lastIdx = math.modf((lower + upper) / 2)211key = state.sortedIdx[state.lastIdx]212if key == control then213break -- key found (and thus prev index)214end215if crossTypeSort(key, control) then216-- key < control, continue search "right" (towards upper bound)217lower = state.lastIdx + 1218else219-- key > control, continue search "left" (towards lower bound)220upper = state.lastIdx - 1221end222until lower > upper223if lower > upper then224-- only true if the key wasn't found, ...225state.lastIdx = state.count -- ... so ensure no match in code below226end227end228229-- proceed by retrieving the next value (or nil) from the sorted keys230state.lastIdx = state.lastIdx + 1231key = state.sortedIdx[state.lastIdx]232if key then233return key, state.t[key]234end235236-- getting here means returning `nil`, which will end the iteration237end238239local function sortedPairs(tbl)240-- Equivalent of the pairs() function on tables. Allows to iterate in241-- sorted order. As required by "generic for" loops, this will return the242-- iterator (function), an "invariant state", and the initial control value.243-- (see http://www.lua.org/pil/7.2.html)244return sortedNext, { t = tbl, sortedIdx = __genSortedIndex(tbl) }, nil245end246M.private.sortedPairs = sortedPairs247248-- seed the random with a strongly varying seed249math.randomseed(math.floor(os.clock() * 1E11))250251local function randomizeTable(t)252-- randomize the item orders of the table t253for i = #t, 2, -1 do254local j = math.random(i)255if i ~= j then256t[i], t[j] = t[j], t[i]257end258end259end260M.private.randomizeTable = randomizeTable261262local function strsplit(delimiter, text)263-- Split text into a list consisting of the strings in text, separated264-- by strings matching delimiter (which may _NOT_ be a pattern).265-- Example: strsplit(", ", "Anna, Bob, Charlie, Dolores")266if delimiter == "" or delimiter == nil then267-- this would result in endless loops268error("delimiter is nil or empty string!")269end270if text == nil then271return nil272end273274local list, pos, first, last = {}, 1275while true do276first, last = text:find(delimiter, pos, true)277if first then278-- found?279table.insert(list, text:sub(pos, first - 1))280pos = last + 1281else282table.insert(list, text:sub(pos))283break284end285end286return list287end288M.private.strsplit = strsplit289290local function hasNewLine(s)291-- return true if s has a newline292return (string.find(s, '\n', 1, true) ~= nil)293end294M.private.hasNewLine = hasNewLine295296local function prefixString(prefix, s)297-- Prefix all the lines of s with prefix298return prefix .. string.gsub(s, '\n', '\n' .. prefix)299end300M.private.prefixString = prefixString301302local function strMatch(s, pattern, start, final)303-- return true if s matches completely the pattern from index start to index end304-- return false in every other cases305-- if start is nil, matches from the beginning of the string306-- if final is nil, matches to the end of the string307start = start or 1308final = final or string.len(s)309310local foundStart, foundEnd = string.find(s, pattern, start, false)311return foundStart == start and foundEnd == final312end313M.private.strMatch = strMatch314315local function patternFilter(patterns, expr)316-- Run `expr` through the inclusion and exclusion rules defined in patterns317-- and return true if expr shall be included, false for excluded.318-- Inclusion pattern are defined as normal patterns, exclusions319-- patterns start with `!` and are followed by a normal pattern320321-- result: nil = UNKNOWN (not matched yet), true = ACCEPT, false = REJECT322-- default: true if no explicit "include" is found, set to false otherwise323local default, result = true, nil324325if patterns ~= nil then326for _, pattern in ipairs(patterns) do327local exclude = pattern:sub(1, 1) == '!'328if exclude then329pattern = pattern:sub(2)330else331-- at least one include pattern specified, a match is required332default = false333end334-- print('pattern: ',pattern)335-- print('exclude: ',exclude)336-- print('default: ',default)337338if string.find(expr, pattern) then339-- set result to false when excluding, true otherwise340result = not exclude341end342end343end344345if result ~= nil then346return result347end348return default349end350M.private.patternFilter = patternFilter351352local function xmlEscape(s)353-- Return s escaped for XML attributes354-- escapes table:355-- " "356-- ' '357-- < <358-- > >359-- & &360361return string.gsub(s, '.', {362['&'] = "&",363['"'] = """,364["'"] = "'",365['<'] = "<",366['>'] = ">",367})368end369M.private.xmlEscape = xmlEscape370371local function xmlCDataEscape(s)372-- Return s escaped for CData section, escapes: "]]>"373return string.gsub(s, ']]>', ']]>')374end375M.private.xmlCDataEscape = xmlCDataEscape376377local function lstrip(s)378--[[Return s with all leading white spaces and tabs removed]]379local idx = 0380while idx < s:len() do381idx = idx + 1382local c = s:sub(idx, idx)383if c ~= ' ' and c ~= '\t' then384break385end386end387return s:sub(idx)388end389M.private.lstrip = lstrip390391local function extractFileLineInfo(s)392--[[ From a string in the form "(leading spaces) dir1/dir2\dir3\file.lua:linenb: msg"393394Return the "file.lua:linenb" information395]]396local s2 = lstrip(s)397local firstColon = s2:find(':', 1, true)398if firstColon == nil then399-- string is not in the format file:line:400return s401end402local secondColon = s2:find(':', firstColon + 1, true)403if secondColon == nil then404-- string is not in the format file:line:405return s406end407408return s2:sub(1, secondColon - 1)409end410M.private.extractFileLineInfo = extractFileLineInfo411412local function stripLuaunitTrace2(stackTrace, errMsg)413--[[414-- Example of a traceback:415<<stack traceback:416example_with_luaunit.lua:130: in function 'test2_withFailure'417./luaunit.lua:1449: in function <./luaunit.lua:1449>418[C]: in function 'xpcall'419./luaunit.lua:1449: in function 'protectedCall'420./luaunit.lua:1508: in function 'execOneFunction'421./luaunit.lua:1596: in function 'runSuiteByInstances'422./luaunit.lua:1660: in function 'runSuiteByNames'423./luaunit.lua:1736: in function 'runSuite'424example_with_luaunit.lua:140: in main chunk425[C]: in ?>>426error message: <<example_with_luaunit.lua:130: expected 2, got 1>>427428Other example:429<<stack traceback:430./luaunit.lua:545: in function 'assertEquals'431example_with_luaunit.lua:58: in function 'TestToto.test7'432./luaunit.lua:1517: in function <./luaunit.lua:1517>433[C]: in function 'xpcall'434./luaunit.lua:1517: in function 'protectedCall'435./luaunit.lua:1578: in function 'execOneFunction'436./luaunit.lua:1677: in function 'runSuiteByInstances'437./luaunit.lua:1730: in function 'runSuiteByNames'438./luaunit.lua:1806: in function 'runSuite'439example_with_luaunit.lua:140: in main chunk440[C]: in ?>>441error message: <<example_with_luaunit.lua:58: expected 2, got 1>>442443<<stack traceback:444luaunit2/example_with_luaunit.lua:124: in function 'test1_withFailure'445luaunit2/luaunit.lua:1532: in function <luaunit2/luaunit.lua:1532>446[C]: in function 'xpcall'447luaunit2/luaunit.lua:1532: in function 'protectedCall'448luaunit2/luaunit.lua:1591: in function 'execOneFunction'449luaunit2/luaunit.lua:1679: in function 'runSuiteByInstances'450luaunit2/luaunit.lua:1743: in function 'runSuiteByNames'451luaunit2/luaunit.lua:1819: in function 'runSuite'452luaunit2/example_with_luaunit.lua:140: in main chunk453[C]: in ?>>454error message: <<luaunit2/example_with_luaunit.lua:124: expected 2, got 1>>455456457-- first line is "stack traceback": KEEP458-- next line may be luaunit line: REMOVE459-- next lines are call in the program under testOk: REMOVE460-- next lines are calls from luaunit to call the program under test: KEEP461462-- Strategy:463-- keep first line464-- remove lines that are part of luaunit465-- kepp lines until we hit a luaunit line466467The strategy for stripping is:468* keep first line "stack traceback:"469* part1:470* analyse all lines of the stack from bottom to top of the stack (first line to last line)471* extract the "file:line:" part of the line472* compare it with the "file:line" part of the error message473* if it does not match strip the line474* if it matches, keep the line and move to part 2475* part2:476* anything NOT starting with luaunit.lua is the interesting part of the stack trace477* anything starting again with luaunit.lua is part of the test launcher and should be stripped out478]]479480local function isLuaunitInternalLine(s)481-- return true if line of stack trace comes from inside luaunit482return s:find('[/\\]luaunit%.lua:%d+: ') ~= nil483end484485-- print( '<<'..stackTrace..'>>' )486487local t = strsplit('\n', stackTrace)488-- print( prettystr(t) )489490local idx = 2491492local errMsgFileLine = extractFileLineInfo(errMsg)493-- print('emfi="'..errMsgFileLine..'"')494495-- remove lines that are still part of luaunit496while t[idx] and extractFileLineInfo(t[idx]) ~= errMsgFileLine do497-- print('Removing : '..t[idx] )498table.remove(t, idx)499end500501-- keep lines until we hit luaunit again502while t[idx] and (not isLuaunitInternalLine(t[idx])) do503-- print('Keeping : '..t[idx] )504idx = idx + 1505end506507-- remove remaining luaunit lines508while t[idx] do509-- print('Removing2 : '..t[idx] )510table.remove(t, idx)511end512513-- print( prettystr(t) )514return table.concat(t, '\n')515516end517M.private.stripLuaunitTrace2 = stripLuaunitTrace2518519local function prettystr_sub(v, indentLevel, printTableRefs, cycleDetectTable)520local type_v = type(v)521if "string" == type_v then522-- use clever delimiters according to content:523-- enclose with single quotes if string contains ", but no '524if v:find('"', 1, true) and not v:find("'", 1, true) then525return "'" .. v .. "'"526end527-- use double quotes otherwise, escape embedded "528return '"' .. v:gsub('"', '\\"') .. '"'529530elseif "table" == type_v then531--if v.__class__ then532-- return string.gsub( tostring(v), 'table', v.__class__ )533--end534return M.private._table_tostring(v, indentLevel, printTableRefs, cycleDetectTable)535536elseif "number" == type_v then537-- eliminate differences in formatting between various Lua versions538if v ~= v then539return "#NaN" -- "not a number"540end541if v == math.huge then542return "#Inf" -- "infinite"543end544if v == -math.huge then545return "-#Inf"546end547if _VERSION == "Lua 5.3" then548local i = math.tointeger(v)549if i then550return tostring(i)551end552end553end554555return tostring(v)556end557558local function prettystr(v)559--[[ Pretty string conversion, to display the full content of a variable of any type.560561* string are enclosed with " by default, or with ' if string contains a "562* tables are expanded to show their full content, with indentation in case of nested tables563]]--564local cycleDetectTable = {}565local s = prettystr_sub(v, 1, M.PRINT_TABLE_REF_IN_ERROR_MSG, cycleDetectTable)566if cycleDetectTable.detected and not M.PRINT_TABLE_REF_IN_ERROR_MSG then567-- some table contain recursive references,568-- so we must recompute the value by including all table references569-- else the result looks like crap570cycleDetectTable = {}571s = prettystr_sub(v, 1, true, cycleDetectTable)572end573return s574end575M.prettystr = prettystr576577function M.adjust_err_msg_with_iter(err_msg, iter_msg)578--[[ Adjust the error message err_msg: trim the FAILURE_PREFIX or SUCCESS_PREFIX information if needed,579add the iteration message if any and return the result.580581err_msg: string, error message captured with pcall582iter_msg: a string describing the current iteration ("iteration N") or nil583if there is no iteration in this test.584585Returns: (new_err_msg, test_status)586new_err_msg: string, adjusted error message, or nil in case of success587test_status: M.NodeStatus.FAIL, SUCCESS or ERROR according to the information588contained in the error message.589]]590if iter_msg then591iter_msg = iter_msg .. ', '592else593iter_msg = ''594end595596local RE_FILE_LINE = '.*:%d+: '597598-- error message is not necessarily a string,599-- so convert the value to string with prettystr()600if type(err_msg) ~= 'string' then601err_msg = prettystr(err_msg)602end603604if (err_msg:find(M.SUCCESS_PREFIX) == 1) or err_msg:match('(' .. RE_FILE_LINE .. ')' .. M.SUCCESS_PREFIX .. ".*") then605-- test finished early with success()606return nil, M.NodeStatus.SUCCESS607end608609if (err_msg:find(M.SKIP_PREFIX) == 1) or (err_msg:match('(' .. RE_FILE_LINE .. ')' .. M.SKIP_PREFIX .. ".*") ~= nil) then610-- substitute prefix by iteration message611err_msg = err_msg:gsub('.*' .. M.SKIP_PREFIX, iter_msg, 1)612-- print("failure detected")613return err_msg, M.NodeStatus.SKIP614end615616if (err_msg:find(M.FAILURE_PREFIX) == 1) or (err_msg:match('(' .. RE_FILE_LINE .. ')' .. M.FAILURE_PREFIX .. ".*") ~= nil) then617-- substitute prefix by iteration message618err_msg = err_msg:gsub(M.FAILURE_PREFIX, iter_msg, 1)619-- print("failure detected")620return err_msg, M.NodeStatus.FAIL621end622623624625-- print("error detected")626-- regular error, not a failure627if iter_msg then628local match629-- "./test\\test_luaunit.lua:2241: some error msg630match = err_msg:match('(.*:%d+: ).*')631if match then632err_msg = err_msg:gsub(match, match .. iter_msg)633else634-- no file:line: infromation, just add the iteration info at the beginning of the line635err_msg = iter_msg .. err_msg636end637end638return err_msg, M.NodeStatus.ERROR639end640641local function tryMismatchFormatting(table_a, table_b, doDeepAnalysis, margin)642--[[643Prepares a nice error message when comparing tables, performing a deeper644analysis.645646Arguments:647* table_a, table_b: tables to be compared648* doDeepAnalysis:649M.DEFAULT_DEEP_ANALYSIS: (the default if not specified) perform deep analysis only for big lists and big dictionnaries650M.FORCE_DEEP_ANALYSIS : always perform deep analysis651M.DISABLE_DEEP_ANALYSIS: never perform deep analysis652* margin: supplied only for almost equality653654Returns: {success, result}655* success: false if deep analysis could not be performed656in this case, just use standard assertion message657* result: if success is true, a multi-line string with deep analysis of the two lists658]]659660-- check if table_a & table_b are suitable for deep analysis661if type(table_a) ~= 'table' or type(table_b) ~= 'table' then662return false663end664665if doDeepAnalysis == M.DISABLE_DEEP_ANALYSIS then666return false667end668669local len_a, len_b, isPureList = #table_a, #table_b, true670671for k1, v1 in pairs(table_a) do672if type(k1) ~= 'number' or k1 > len_a then673-- this table a mapping674isPureList = false675break676end677end678679if isPureList then680for k2, v2 in pairs(table_b) do681if type(k2) ~= 'number' or k2 > len_b then682-- this table a mapping683isPureList = false684break685end686end687end688689if isPureList and math.min(len_a, len_b) < M.LIST_DIFF_ANALYSIS_THRESHOLD then690if not (doDeepAnalysis == M.FORCE_DEEP_ANALYSIS) then691return false692end693end694695if isPureList then696return M.private.mismatchFormattingPureList(table_a, table_b, margin)697else698-- only work on mapping for the moment699-- return M.private.mismatchFormattingMapping( table_a, table_b, doDeepAnalysis )700return false701end702end703M.private.tryMismatchFormatting = tryMismatchFormatting704705local function getTaTbDescr()706if not M.ORDER_ACTUAL_EXPECTED then707return 'expected', 'actual'708end709return 'actual', 'expected'710end711712local function extendWithStrFmt(res, ...)713table.insert(res, string.format(...))714end715716local function mismatchFormattingMapping(table_a, table_b, doDeepAnalysis)717--[[718Prepares a nice error message when comparing tables which are not pure lists, performing a deeper719analysis.720721Returns: {success, result}722* success: false if deep analysis could not be performed723in this case, just use standard assertion message724* result: if success is true, a multi-line string with deep analysis of the two lists725]]726727-- disable for the moment728--[[729local result = {}730local descrTa, descrTb = getTaTbDescr()731732local keysCommon = {}733local keysOnlyTa = {}734local keysOnlyTb = {}735local keysDiffTaTb = {}736737local k, v738739for k,v in pairs( table_a ) do740if is_equal( v, table_b[k] ) then741table.insert( keysCommon, k )742else743if table_b[k] == nil then744table.insert( keysOnlyTa, k )745else746table.insert( keysDiffTaTb, k )747end748end749end750751for k,v in pairs( table_b ) do752if not is_equal( v, table_a[k] ) and table_a[k] == nil then753table.insert( keysOnlyTb, k )754end755end756757local len_a = #keysCommon + #keysDiffTaTb + #keysOnlyTa758local len_b = #keysCommon + #keysDiffTaTb + #keysOnlyTb759local limited_display = (len_a < 5 or len_b < 5)760761if math.min(len_a, len_b) < M.TABLE_DIFF_ANALYSIS_THRESHOLD then762return false763end764765if not limited_display then766if len_a == len_b then767extendWithStrFmt( result, 'Table A (%s) and B (%s) both have %d items', descrTa, descrTb, len_a )768else769extendWithStrFmt( result, 'Table A (%s) has %d items and table B (%s) has %d items', descrTa, len_a, descrTb, len_b )770end771772if #keysCommon == 0 and #keysDiffTaTb == 0 then773table.insert( result, 'Table A and B have no keys in common, they are totally different')774else775local s_other = 'other '776if #keysCommon then777extendWithStrFmt( result, 'Table A and B have %d identical items', #keysCommon )778else779table.insert( result, 'Table A and B have no identical items' )780s_other = ''781end782783if #keysDiffTaTb ~= 0 then784result[#result] = string.format( '%s and %d items differing present in both tables', result[#result], #keysDiffTaTb)785else786result[#result] = string.format( '%s and no %sitems differing present in both tables', result[#result], s_other, #keysDiffTaTb)787end788end789790extendWithStrFmt( result, 'Table A has %d keys not present in table B and table B has %d keys not present in table A', #keysOnlyTa, #keysOnlyTb )791end792793local function keytostring(k)794if "string" == type(k) and k:match("^[_%a][_%w]*$") then795return k796end797return prettystr(k)798end799800if #keysDiffTaTb ~= 0 then801table.insert( result, 'Items differing in A and B:')802for k,v in sortedPairs( keysDiffTaTb ) do803extendWithStrFmt( result, ' - A[%s]: %s', keytostring(v), prettystr(table_a[v]) )804extendWithStrFmt( result, ' + B[%s]: %s', keytostring(v), prettystr(table_b[v]) )805end806end807808if #keysOnlyTa ~= 0 then809table.insert( result, 'Items only in table A:' )810for k,v in sortedPairs( keysOnlyTa ) do811extendWithStrFmt( result, ' - A[%s]: %s', keytostring(v), prettystr(table_a[v]) )812end813end814815if #keysOnlyTb ~= 0 then816table.insert( result, 'Items only in table B:' )817for k,v in sortedPairs( keysOnlyTb ) do818extendWithStrFmt( result, ' + B[%s]: %s', keytostring(v), prettystr(table_b[v]) )819end820end821822if #keysCommon ~= 0 then823table.insert( result, 'Items common to A and B:')824for k,v in sortedPairs( keysCommon ) do825extendWithStrFmt( result, ' = A and B [%s]: %s', keytostring(v), prettystr(table_a[v]) )826end827end828829return true, table.concat( result, '\n')830]]831end832M.private.mismatchFormattingMapping = mismatchFormattingMapping833834local function mismatchFormattingPureList(table_a, table_b, margin)835--[[836Prepares a nice error message when comparing tables which are lists, performing a deeper837analysis.838839margin is supplied only for almost equality840841Returns: {success, result}842* success: false if deep analysis could not be performed843in this case, just use standard assertion message844* result: if success is true, a multi-line string with deep analysis of the two lists845]]846local result, descrTa, descrTb = {}, getTaTbDescr()847848local len_a, len_b, refa, refb = #table_a, #table_b, '', ''849if M.PRINT_TABLE_REF_IN_ERROR_MSG then850refa, refb = string.format('<%s> ', M.private.table_ref(table_a)), string.format('<%s> ', M.private.table_ref(table_b))851end852local longest, shortest = math.max(len_a, len_b), math.min(len_a, len_b)853local deltalv = longest - shortest854855local commonUntil = shortest856for i = 1, shortest do857if not M.private.is_table_equals(table_a[i], table_b[i], margin) then858commonUntil = i - 1859break860end861end862863local commonBackTo = shortest - 1864for i = 0, shortest - 1 do865if not M.private.is_table_equals(table_a[len_a - i], table_b[len_b - i], margin) then866commonBackTo = i - 1867break868end869end870871table.insert(result, 'List difference analysis:')872if len_a == len_b then873extendWithStrFmt(result, '* lists %sA (%s) and %sB (%s) have the same size', refa, descrTa, refb, descrTb)874else875extendWithStrFmt(result, '* list sizes differ: list %sA (%s) has %d items, list %sB (%s) has %d items', refa, descrTa, len_a, refb, descrTb, len_b)876end877878extendWithStrFmt(result, '* lists A and B start differing at index %d', commonUntil + 1)879if commonBackTo >= 0 then880if deltalv > 0 then881extendWithStrFmt(result, '* lists A and B are equal again from index %d for A, %d for B', len_a - commonBackTo, len_b - commonBackTo)882else883extendWithStrFmt(result, '* lists A and B are equal again from index %d', len_a - commonBackTo)884end885end886887local function insertABValue(ai, bi)888bi = bi or ai889if M.private.is_table_equals(table_a[ai], table_b[bi], margin) then890return extendWithStrFmt(result, ' = A[%d], B[%d]: %s', ai, bi, prettystr(table_a[ai]))891else892extendWithStrFmt(result, ' - A[%d]: %s', ai, prettystr(table_a[ai]))893extendWithStrFmt(result, ' + B[%d]: %s', bi, prettystr(table_b[bi]))894end895end896897-- common parts to list A & B, at the beginning898if commonUntil > 0 then899table.insert(result, '* Common parts:')900for i = 1, commonUntil do901insertABValue(i)902end903end904905-- diffing parts to list A & B906if commonUntil < shortest - commonBackTo - 1 then907table.insert(result, '* Differing parts:')908for i = commonUntil + 1, shortest - commonBackTo - 1 do909insertABValue(i)910end911end912913-- display indexes of one list, with no match on other list914if shortest - commonBackTo <= longest - commonBackTo - 1 then915table.insert(result, '* Present only in one list:')916for i = shortest - commonBackTo, longest - commonBackTo - 1 do917if len_a > len_b then918extendWithStrFmt(result, ' - A[%d]: %s', i, prettystr(table_a[i]))919-- table.insert( result, '+ (no matching B index)')920else921-- table.insert( result, '- no matching A index')922extendWithStrFmt(result, ' + B[%d]: %s', i, prettystr(table_b[i]))923end924end925end926927-- common parts to list A & B, at the end928if commonBackTo >= 0 then929table.insert(result, '* Common parts at the end of the lists')930for i = longest - commonBackTo, longest do931if len_a > len_b then932insertABValue(i, i - deltalv)933else934insertABValue(i - deltalv, i)935end936end937end938939return true, table.concat(result, '\n')940end941M.private.mismatchFormattingPureList = mismatchFormattingPureList942943local function prettystrPairs(value1, value2, suffix_a, suffix_b)944--[[945This function helps with the recurring task of constructing the "expected946vs. actual" error messages. It takes two arbitrary values and formats947corresponding strings with prettystr().948949To keep the (possibly complex) output more readable in case the resulting950strings contain line breaks, they get automatically prefixed with additional951newlines. Both suffixes are optional (default to empty strings), and get952appended to the "value1" string. "suffix_a" is used if line breaks were953encountered, "suffix_b" otherwise.954955Returns the two formatted strings (including padding/newlines).956]]957local str1, str2 = prettystr(value1), prettystr(value2)958if hasNewLine(str1) or hasNewLine(str2) then959-- line break(s) detected, add padding960return "\n" .. str1 .. (suffix_a or ""), "\n" .. str2961end962return str1 .. (suffix_b or ""), str2963end964M.private.prettystrPairs = prettystrPairs965966local UNKNOWN_REF = 'table 00-unknown ref'967local ref_generator = { value = 1, [UNKNOWN_REF] = 0 }968969local function table_ref(t)970-- return the default tostring() for tables, with the table ID, even if the table has a metatable971-- with the __tostring converter972local ref = ''973local mt = getmetatable(t)974if mt == nil then975ref = tostring(t)976else977local success, result978success, result = pcall(setmetatable, t, nil)979if not success then980-- protected table, if __tostring is defined, we can981-- not get the reference. And we can not know in advance.982ref = tostring(t)983if not ref:match('table: 0?x?[%x]+') then984return UNKNOWN_REF985end986else987ref = tostring(t)988setmetatable(t, mt)989end990end991-- strip the "table: " part992ref = ref:sub(8)993if ref ~= UNKNOWN_REF and ref_generator[ref] == nil then994-- Create a new reference number995ref_generator[ref] = ref_generator.value996ref_generator.value = ref_generator.value + 1997end998if M.PRINT_TABLE_REF_IN_ERROR_MSG then999return string.format('table %02d-%s', ref_generator[ref], ref)1000else1001return string.format('table %02d', ref_generator[ref])1002end1003end1004M.private.table_ref = table_ref10051006local TABLE_TOSTRING_SEP = ", "1007local TABLE_TOSTRING_SEP_LEN = string.len(TABLE_TOSTRING_SEP)10081009local function _table_tostring(tbl, indentLevel, printTableRefs, cycleDetectTable)1010printTableRefs = printTableRefs or M.PRINT_TABLE_REF_IN_ERROR_MSG1011cycleDetectTable = cycleDetectTable or {}1012cycleDetectTable[tbl] = true10131014local result, dispOnMultLines = {}, false10151016-- like prettystr but do not enclose with "" if the string is just alphanumerical1017-- this is better for displaying table keys who are often simple strings1018local function keytostring(k)1019if "string" == type(k) and k:match("^[_%a][_%w]*$") then1020return k1021end1022return prettystr_sub(k, indentLevel + 1, printTableRefs, cycleDetectTable)1023end10241025local mt = getmetatable(tbl)10261027if mt and mt.__tostring then1028-- if table has a __tostring() function in its metatable, use it to display the table1029-- else, compute a regular table1030result = tostring(tbl)1031if type(result) ~= 'string' then1032return string.format('<invalid tostring() result: "%s" >', prettystr(result))1033end1034result = strsplit('\n', result)1035return M.private._table_tostring_format_multiline_string(result, indentLevel)10361037else1038-- no metatable, compute the table representation10391040local entry, count, seq_index = nil, 0, 11041for k, v in sortedPairs(tbl) do10421043-- key part1044if k == seq_index then1045-- for the sequential part of tables, we'll skip the "<key>=" output1046entry = ''1047seq_index = seq_index + 11048elseif cycleDetectTable[k] then1049-- recursion in the key detected1050cycleDetectTable.detected = true1051entry = "<" .. table_ref(k) .. ">="1052else1053entry = keytostring(k) .. "="1054end10551056-- value part1057if cycleDetectTable[v] then1058-- recursion in the value detected!1059cycleDetectTable.detected = true1060entry = entry .. "<" .. table_ref(v) .. ">"1061else1062entry = entry ..1063prettystr_sub(v, indentLevel + 1, printTableRefs, cycleDetectTable)1064end1065count = count + 11066result[count] = entry1067end1068return M.private._table_tostring_format_result(tbl, result, indentLevel, printTableRefs)1069end10701071end1072M.private._table_tostring = _table_tostring -- prettystr_sub() needs it10731074local function _table_tostring_format_multiline_string(tbl_str, indentLevel)1075local indentString = '\n' .. string.rep(" ", indentLevel - 1)1076return table.concat(tbl_str, indentString)10771078end1079M.private._table_tostring_format_multiline_string = _table_tostring_format_multiline_string10801081local function _table_tostring_format_result(tbl, result, indentLevel, printTableRefs)1082-- final function called in _table_to_string() to format the resulting list of1083-- string describing the table.10841085local dispOnMultLines = false10861087-- set dispOnMultLines to true if the maximum LINE_LENGTH would be exceeded with the values1088local totalLength = 01089for k, v in ipairs(result) do1090totalLength = totalLength + string.len(v)1091if totalLength >= M.LINE_LENGTH then1092dispOnMultLines = true1093break1094end1095end10961097-- set dispOnMultLines to true if the max LINE_LENGTH would be exceeded1098-- with the values and the separators.1099if not dispOnMultLines then1100-- adjust with length of separator(s):1101-- two items need 1 sep, three items two seps, ... plus len of '{}'1102if #result > 0 then1103totalLength = totalLength + TABLE_TOSTRING_SEP_LEN * (#result - 1)1104end1105dispOnMultLines = (totalLength + 2 >= M.LINE_LENGTH)1106end11071108-- now reformat the result table (currently holding element strings)1109if dispOnMultLines then1110local indentString = string.rep(" ", indentLevel - 1)1111result = {1112"{\n ",1113indentString,1114table.concat(result, ",\n " .. indentString),1115"\n",1116indentString,1117"}"1118}1119else1120result = { "{", table.concat(result, TABLE_TOSTRING_SEP), "}" }1121end1122if printTableRefs then1123table.insert(result, 1, "<" .. table_ref(tbl) .. "> ") -- prepend table ref1124end1125return table.concat(result)1126end1127M.private._table_tostring_format_result = _table_tostring_format_result -- prettystr_sub() needs it11281129local function table_findkeyof(t, element)1130-- Return the key k of the given element in table t, so that t[k] == element1131-- (or `nil` if element is not present within t). Note that we use our1132-- 'general' is_equal comparison for matching, so this function should1133-- handle table-type elements gracefully and consistently.1134if type(t) == "table" then1135for k, v in pairs(t) do1136if M.private.is_table_equals(v, element) then1137return k1138end1139end1140end1141return nil1142end11431144local function _is_table_items_equals(actual, expected)1145local type_a, type_e = type(actual), type(expected)11461147if type_a ~= type_e then1148return false11491150elseif (type_a == 'table') --[[and (type_e == 'table')]] then1151for k, v in pairs(actual) do1152if table_findkeyof(expected, v) == nil then1153return false -- v not contained in expected1154end1155end1156for k, v in pairs(expected) do1157if table_findkeyof(actual, v) == nil then1158return false -- v not contained in actual1159end1160end1161return true11621163elseif actual ~= expected then1164return false1165end11661167return true1168end11691170--[[1171This is a specialized metatable to help with the bookkeeping of recursions1172in _is_table_equals(). It provides an __index table that implements utility1173functions for easier management of the table. The "cached" method queries1174the state of a specific (actual,expected) pair; and the "store" method sets1175this state to the given value. The state of pairs not "seen" / visited is1176assumed to be `nil`.1177]]1178local _recursion_cache_MT = {1179__index = {1180-- Return the cached value for an (actual,expected) pair (or `nil`)1181cached = function(t, actual, expected)1182local subtable = t[actual] or {}1183return subtable[expected]1184end,11851186-- Store cached value for a specific (actual,expected) pair.1187-- Returns the value, so it's easy to use for a "tailcall" (return ...).1188store = function(t, actual, expected, value, asymmetric)1189local subtable = t[actual]1190if not subtable then1191subtable = {}1192t[actual] = subtable1193end1194subtable[expected] = value11951196-- Unless explicitly marked "asymmetric": Consider the recursion1197-- on (expected,actual) to be equivalent to (actual,expected) by1198-- default, and thus cache the value for both.1199if not asymmetric then1200t:store(expected, actual, value, true)1201end12021203return value1204end1205}1206}12071208local function _is_table_equals(actual, expected, cycleDetectTable, marginForAlmostEqual)1209--[[Returns true if both table are equal.12101211If argument marginForAlmostEqual is suppied, number comparison is done using alomstEqual instead1212of strict equality.12131214cycleDetectTable is an internal argument used during recursion on tables.1215]]1216--print('_is_table_equals( \n '..prettystr(actual)..'\n , '..prettystr(expected)..1217-- '\n , '..prettystr(cycleDetectTable)..'\n , '..prettystr(marginForAlmostEqual)..' )')12181219local type_a, type_e = type(actual), type(expected)12201221if type_a ~= type_e then1222return false -- different types won't match1223end12241225if type_a == 'number' then1226if marginForAlmostEqual ~= nil then1227return M.almostEquals(actual, expected, marginForAlmostEqual)1228else1229return actual == expected1230end1231elseif type_a ~= 'table' then1232-- other types compare directly1233return actual == expected1234end12351236cycleDetectTable = cycleDetectTable or { actual = {}, expected = {} }1237if cycleDetectTable.actual[actual] then1238-- oh, we hit a cycle in actual1239if cycleDetectTable.expected[expected] then1240-- uh, we hit a cycle at the same time in expected1241-- so the two tables have similar structure1242return true1243end12441245-- cycle was hit only in actual, the structure differs from expected1246return false1247end12481249if cycleDetectTable.expected[expected] then1250-- no cycle in actual, but cycle in expected1251-- the structure differ1252return false1253end12541255-- at this point, no table cycle detected, we are1256-- seeing this table for the first time12571258-- mark the cycle detection1259cycleDetectTable.actual[actual] = true1260cycleDetectTable.expected[expected] = true12611262local actualKeysMatched = {}1263for k, v in pairs(actual) do1264actualKeysMatched[k] = true -- Keep track of matched keys1265if not _is_table_equals(v, expected[k], cycleDetectTable, marginForAlmostEqual) then1266-- table differs on this key1267-- clear the cycle detection before returning1268cycleDetectTable.actual[actual] = nil1269cycleDetectTable.expected[expected] = nil1270return false1271end1272end12731274for k, v in pairs(expected) do1275if not actualKeysMatched[k] then1276-- Found a key that we did not see in "actual" -> mismatch1277-- clear the cycle detection before returning1278cycleDetectTable.actual[actual] = nil1279cycleDetectTable.expected[expected] = nil1280return false1281end1282-- Otherwise actual[k] was already matched against v = expected[k].1283end12841285-- all key match, we have a match !1286cycleDetectTable.actual[actual] = nil1287cycleDetectTable.expected[expected] = nil1288return true1289end1290M.private._is_table_equals = _is_table_equals12911292local function failure(main_msg, extra_msg_or_nil, level)1293-- raise an error indicating a test failure1294-- for error() compatibility we adjust "level" here (by +1), to report the1295-- calling context1296local msg1297if type(extra_msg_or_nil) == 'string' and extra_msg_or_nil:len() > 0 then1298msg = extra_msg_or_nil .. '\n' .. main_msg1299else1300msg = main_msg1301end1302error(M.FAILURE_PREFIX .. msg, (level or 1) + 1 + M.STRIP_EXTRA_ENTRIES_IN_STACK_TRACE)1303end13041305local function is_table_equals(actual, expected, marginForAlmostEqual)1306return _is_table_equals(actual, expected, nil, marginForAlmostEqual)1307end1308M.private.is_table_equals = is_table_equals13091310local function fail_fmt(level, extra_msg_or_nil, ...)1311-- failure with printf-style formatted message and given error level1312failure(string.format(...), extra_msg_or_nil, (level or 1) + 1)1313end1314M.private.fail_fmt = fail_fmt13151316local function error_fmt(level, ...)1317-- printf-style error()1318error(string.format(...), (level or 1) + 1 + M.STRIP_EXTRA_ENTRIES_IN_STACK_TRACE)1319end1320M.private.error_fmt = error_fmt13211322----------------------------------------------------------------1323--1324-- assertions1325--1326----------------------------------------------------------------13271328local function errorMsgEquality(actual, expected, doDeepAnalysis, margin)1329-- margin is supplied only for almost equal verification13301331if not M.ORDER_ACTUAL_EXPECTED then1332expected, actual = actual, expected1333end1334if type(expected) == 'string' or type(expected) == 'table' then1335local strExpected, strActual = prettystrPairs(expected, actual)1336local result = string.format("expected: %s\nactual: %s", strExpected, strActual)1337if margin then1338result = result .. '\nwere not equal by the margin of: ' .. prettystr(margin)1339end13401341-- extend with mismatch analysis if possible:1342local success, mismatchResult1343success, mismatchResult = tryMismatchFormatting(actual, expected, doDeepAnalysis, margin)1344if success then1345result = table.concat({ result, mismatchResult }, '\n')1346end1347return result1348end1349return string.format("expected: %s, actual: %s",1350prettystr(expected), prettystr(actual))1351end13521353function M.assertError(f, ...)1354-- assert that calling f with the arguments will raise an error1355-- example: assertError( f, 1, 2 ) => f(1,2) should generate an error1356if pcall(f, ...) then1357failure("Expected an error when calling function but no error generated", nil, 2)1358end1359end13601361function M.fail(msg)1362-- stops a test due to a failure1363failure(msg, nil, 2)1364end13651366function M.failIf(cond, msg)1367-- Fails a test with "msg" if condition is true1368if cond then1369failure(msg, nil, 2)1370end1371end13721373function M.skip(msg)1374-- skip a running test1375error_fmt(2, M.SKIP_PREFIX .. msg)1376end13771378function M.skipIf(cond, msg)1379-- skip a running test if condition is met1380if cond then1381error_fmt(2, M.SKIP_PREFIX .. msg)1382end1383end13841385function M.runOnlyIf(cond, msg)1386-- continue a running test if condition is met, else skip it1387if not cond then1388error_fmt(2, M.SKIP_PREFIX .. prettystr(msg))1389end1390end13911392function M.success()1393-- stops a test with a success1394error_fmt(2, M.SUCCESS_PREFIX)1395end13961397function M.successIf(cond)1398-- stops a test with a success if condition is met1399if cond then1400error_fmt(2, M.SUCCESS_PREFIX)1401end1402end140314041405------------------------------------------------------------------1406-- Equality assertions1407------------------------------------------------------------------14081409function M.assertEquals(actual, expected, extra_msg_or_nil, doDeepAnalysis)1410if type(actual) == 'table' and type(expected) == 'table' then1411if not is_table_equals(actual, expected) then1412failure(errorMsgEquality(actual, expected, doDeepAnalysis), extra_msg_or_nil, 2)1413end1414elseif type(actual) ~= type(expected) then1415failure(errorMsgEquality(actual, expected), extra_msg_or_nil, 2)1416elseif actual ~= expected then1417failure(errorMsgEquality(actual, expected), extra_msg_or_nil, 2)1418end1419end14201421function M.almostEquals(actual, expected, margin)1422if type(actual) ~= 'number' or type(expected) ~= 'number' or type(margin) ~= 'number' then1423error_fmt(3, 'almostEquals: must supply only number arguments.\nArguments supplied: %s, %s, %s',1424prettystr(actual), prettystr(expected), prettystr(margin))1425end1426if margin < 0 then1427error_fmt(3, 'almostEquals: margin must not be negative, current value is ' .. margin)1428end1429return math.abs(expected - actual) <= margin1430end14311432function M.assertAlmostEquals(actual, expected, margin, extra_msg_or_nil)1433-- check that two floats are close by margin1434margin = margin or M.EPS1435if type(margin) ~= 'number' then1436error_fmt(2, 'almostEquals: margin must be a number, not %s', prettystr(margin))1437end14381439if type(actual) == 'table' and type(expected) == 'table' then1440-- handle almost equals for table1441if not is_table_equals(actual, expected, margin) then1442failure(errorMsgEquality(actual, expected, nil, margin), extra_msg_or_nil, 2)1443end1444elseif type(actual) == 'number' and type(expected) == 'number' and type(margin) == 'number' then1445if not M.almostEquals(actual, expected, margin) then1446if not M.ORDER_ACTUAL_EXPECTED then1447expected, actual = actual, expected1448end1449local delta = math.abs(actual - expected)1450fail_fmt(2, extra_msg_or_nil, 'Values are not almost equal\n' ..1451'Actual: %s, expected: %s, delta %s above margin of %s',1452actual, expected, delta, margin)1453end1454else1455error_fmt(3, 'almostEquals: must supply only number or table arguments.\nArguments supplied: %s, %s, %s',1456prettystr(actual), prettystr(expected), prettystr(margin))1457end1458end14591460function M.assertNotEquals(actual, expected, extra_msg_or_nil)1461if type(actual) ~= type(expected) then1462return1463end14641465if type(actual) == 'table' and type(expected) == 'table' then1466if not is_table_equals(actual, expected) then1467return1468end1469elseif actual ~= expected then1470return1471end1472fail_fmt(2, extra_msg_or_nil, 'Received the not expected value: %s', prettystr(actual))1473end14741475function M.assertNotAlmostEquals(actual, expected, margin, extra_msg_or_nil)1476-- check that two floats are not close by margin1477margin = margin or M.EPS1478if M.almostEquals(actual, expected, margin) then1479if not M.ORDER_ACTUAL_EXPECTED then1480expected, actual = actual, expected1481end1482local delta = math.abs(actual - expected)1483fail_fmt(2, extra_msg_or_nil, 'Values are almost equal\nActual: %s, expected: %s' ..1484', delta %s below margin of %s',1485actual, expected, delta, margin)1486end1487end14881489function M.assertItemsEquals(actual, expected, extra_msg_or_nil)1490-- checks that the items of table expected1491-- are contained in table actual. Warning, this function1492-- is at least O(n^2)1493if not _is_table_items_equals(actual, expected) then1494expected, actual = prettystrPairs(expected, actual)1495fail_fmt(2, extra_msg_or_nil, 'Content of the tables are not identical:\nExpected: %s\nActual: %s',1496expected, actual)1497end1498end14991500------------------------------------------------------------------1501-- String assertion1502------------------------------------------------------------------15031504function M.assertStrContains(str, sub, isPattern, extra_msg_or_nil)1505-- this relies on lua string.find function1506-- a string always contains the empty string1507-- assert( type(str) == 'string', 'Argument 1 of assertStrContains() should be a string.' ) )1508-- assert( type(sub) == 'string', 'Argument 2 of assertStrContains() should be a string.' ) )1509if not string.find(str, sub, 1, not isPattern) then1510sub, str = prettystrPairs(sub, str, '\n')1511fail_fmt(2, extra_msg_or_nil, 'Could not find %s %s in string %s',1512isPattern and 'pattern' or 'substring', sub, str)1513end1514end15151516function M.assertStrIContains(str, sub, extra_msg_or_nil)1517-- this relies on lua string.find function1518-- a string always contains the empty string1519if not string.find(str:lower(), sub:lower(), 1, true) then1520sub, str = prettystrPairs(sub, str, '\n')1521fail_fmt(2, extra_msg_or_nil, 'Could not find (case insensitively) substring %s in string %s',1522sub, str)1523end1524end15251526function M.assertNotStrContains(str, sub, isPattern, extra_msg_or_nil)1527-- this relies on lua string.find function1528-- a string always contains the empty string1529if string.find(str, sub, 1, not isPattern) then1530sub, str = prettystrPairs(sub, str, '\n')1531fail_fmt(2, extra_msg_or_nil, 'Found the not expected %s %s in string %s',1532isPattern and 'pattern' or 'substring', sub, str)1533end1534end15351536function M.assertNotStrIContains(str, sub, extra_msg_or_nil)1537-- this relies on lua string.find function1538-- a string always contains the empty string1539if string.find(str:lower(), sub:lower(), 1, true) then1540sub, str = prettystrPairs(sub, str, '\n')1541fail_fmt(2, extra_msg_or_nil, 'Found (case insensitively) the not expected substring %s in string %s',1542sub, str)1543end1544end15451546function M.assertStrMatches(str, pattern, start, final, extra_msg_or_nil)1547-- Verify a full match for the string1548if not strMatch(str, pattern, start, final) then1549pattern, str = prettystrPairs(pattern, str, '\n')1550fail_fmt(2, extra_msg_or_nil, 'Could not match pattern %s with string %s',1551pattern, str)1552end1553end15541555local function _assertErrorMsgEquals(stripFileAndLine, expectedMsg, func, ...)1556local no_error, error_msg = pcall(func, ...)1557if no_error then1558failure('No error generated when calling function but expected error: ' .. M.prettystr(expectedMsg), nil, 3)1559end1560if type(expectedMsg) == "string" and type(error_msg) ~= "string" then1561-- table are converted to string automatically1562error_msg = tostring(error_msg)1563end1564local differ = false1565if stripFileAndLine then1566if error_msg:gsub("^.+:%d+: ", "") ~= expectedMsg then1567differ = true1568end1569else1570if error_msg ~= expectedMsg then1571local tr = type(error_msg)1572local te = type(expectedMsg)1573if te == 'table' then1574if tr ~= 'table' then1575differ = true1576else1577local ok = pcall(M.assertItemsEquals, error_msg, expectedMsg)1578if not ok then1579differ = true1580end1581end1582else1583differ = true1584end1585end1586end15871588if differ then1589error_msg, expectedMsg = prettystrPairs(error_msg, expectedMsg)1590fail_fmt(3, nil, 'Error message expected: %s\nError message received: %s\n',1591expectedMsg, error_msg)1592end1593end15941595function M.assertErrorMsgEquals(expectedMsg, func, ...)1596-- assert that calling f with the arguments will raise an error1597-- example: assertError( f, 1, 2 ) => f(1,2) should generate an error1598_assertErrorMsgEquals(false, expectedMsg, func, ...)1599end16001601function M.assertErrorMsgContentEquals(expectedMsg, func, ...)1602_assertErrorMsgEquals(true, expectedMsg, func, ...)1603end16041605function M.assertErrorMsgContains(partialMsg, func, ...)1606-- assert that calling f with the arguments will raise an error1607-- example: assertError( f, 1, 2 ) => f(1,2) should generate an error1608local no_error, error_msg = pcall(func, ...)1609if no_error then1610failure('No error generated when calling function but expected error containing: ' .. prettystr(partialMsg), nil, 2)1611end1612if type(error_msg) ~= "string" then1613error_msg = tostring(error_msg)1614end1615if not string.find(error_msg, partialMsg, nil, true) then1616error_msg, partialMsg = prettystrPairs(error_msg, partialMsg)1617fail_fmt(2, nil, 'Error message does not contain: %s\nError message received: %s\n',1618partialMsg, error_msg)1619end1620end16211622function M.assertErrorMsgMatches(expectedMsg, func, ...)1623-- assert that calling f with the arguments will raise an error1624-- example: assertError( f, 1, 2 ) => f(1,2) should generate an error1625local no_error, error_msg = pcall(func, ...)1626if no_error then1627failure('No error generated when calling function but expected error matching: "' .. expectedMsg .. '"', nil, 2)1628end1629if type(error_msg) ~= "string" then1630error_msg = tostring(error_msg)1631end1632if not strMatch(error_msg, expectedMsg) then1633expectedMsg, error_msg = prettystrPairs(expectedMsg, error_msg)1634fail_fmt(2, nil, 'Error message does not match pattern: %s\nError message received: %s\n',1635expectedMsg, error_msg)1636end1637end16381639------------------------------------------------------------------1640-- Type assertions1641------------------------------------------------------------------16421643function M.assertEvalToTrue(value, extra_msg_or_nil)1644if not value then1645failure("expected: a value evaluating to true, actual: " .. prettystr(value), extra_msg_or_nil, 2)1646end1647end16481649function M.assertEvalToFalse(value, extra_msg_or_nil)1650if value then1651failure("expected: false or nil, actual: " .. prettystr(value), extra_msg_or_nil, 2)1652end1653end16541655function M.assertIsTrue(value, extra_msg_or_nil)1656if value ~= true then1657failure("expected: true, actual: " .. prettystr(value), extra_msg_or_nil, 2)1658end1659end16601661function M.assertNotIsTrue(value, extra_msg_or_nil)1662if value == true then1663failure("expected: not true, actual: " .. prettystr(value), extra_msg_or_nil, 2)1664end1665end16661667function M.assertIsFalse(value, extra_msg_or_nil)1668if value ~= false then1669failure("expected: false, actual: " .. prettystr(value), extra_msg_or_nil, 2)1670end1671end16721673function M.assertNotIsFalse(value, extra_msg_or_nil)1674if value == false then1675failure("expected: not false, actual: " .. prettystr(value), extra_msg_or_nil, 2)1676end1677end16781679function M.assertIsNil(value, extra_msg_or_nil)1680if value ~= nil then1681failure("expected: nil, actual: " .. prettystr(value), extra_msg_or_nil, 2)1682end1683end16841685function M.assertNotIsNil(value, extra_msg_or_nil)1686if value == nil then1687failure("expected: not nil, actual: nil", extra_msg_or_nil, 2)1688end1689end16901691--[[1692Add type assertion functions to the module table M. Each of these functions1693takes a single parameter "value", and checks that its Lua type matches the1694expected string (derived from the function name):16951696M.assertIsXxx(value) -> ensure that type(value) conforms to "xxx"1697]]1698for _, funcName in ipairs(1699{ 'assertIsNumber', 'assertIsString', 'assertIsTable', 'assertIsBoolean',1700'assertIsFunction', 'assertIsUserdata', 'assertIsThread' }1701) do1702local typeExpected = funcName:match("^assertIs([A-Z]%a*)$")1703-- Lua type() always returns lowercase, also make sure the match() succeeded1704typeExpected = typeExpected and typeExpected:lower()1705or error("bad function name '" .. funcName .. "' for type assertion")17061707M[funcName] = function(value, extra_msg_or_nil)1708if type(value) ~= typeExpected then1709if type(value) == 'nil' then1710fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: nil',1711typeExpected, type(value), prettystrPairs(value))1712else1713fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: type %s, value %s',1714typeExpected, type(value), prettystrPairs(value))1715end1716end1717end1718end17191720--[[1721Add shortcuts for verifying type of a variable, without failure (luaunit v2 compatibility)1722M.isXxx(value) -> returns true if type(value) conforms to "xxx"1723]]1724for _, typeExpected in ipairs(1725{ 'Number', 'String', 'Table', 'Boolean',1726'Function', 'Userdata', 'Thread', 'Nil' }1727) do1728local typeExpectedLower = typeExpected:lower()1729local isType = function(value)1730return (type(value) == typeExpectedLower)1731end1732M['is' .. typeExpected] = isType1733M['is_' .. typeExpectedLower] = isType1734end17351736--[[1737Add non-type assertion functions to the module table M. Each of these functions1738takes a single parameter "value", and checks that its Lua type differs from the1739expected string (derived from the function name):17401741M.assertNotIsXxx(value) -> ensure that type(value) is not "xxx"1742]]1743for _, funcName in ipairs(1744{ 'assertNotIsNumber', 'assertNotIsString', 'assertNotIsTable', 'assertNotIsBoolean',1745'assertNotIsFunction', 'assertNotIsUserdata', 'assertNotIsThread' }1746) do1747local typeUnexpected = funcName:match("^assertNotIs([A-Z]%a*)$")1748-- Lua type() always returns lowercase, also make sure the match() succeeded1749typeUnexpected = typeUnexpected and typeUnexpected:lower()1750or error("bad function name '" .. funcName .. "' for type assertion")17511752M[funcName] = function(value, extra_msg_or_nil)1753if type(value) == typeUnexpected then1754fail_fmt(2, extra_msg_or_nil, 'expected: not a %s type, actual: value %s',1755typeUnexpected, prettystrPairs(value))1756end1757end1758end17591760function M.assertIs(actual, expected, extra_msg_or_nil)1761if actual ~= expected then1762if not M.ORDER_ACTUAL_EXPECTED then1763actual, expected = expected, actual1764end1765local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG1766M.PRINT_TABLE_REF_IN_ERROR_MSG = true1767expected, actual = prettystrPairs(expected, actual, '\n', '')1768M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg1769fail_fmt(2, extra_msg_or_nil, 'expected and actual object should not be different\nExpected: %s\nReceived: %s',1770expected, actual)1771end1772end17731774function M.assertNotIs(actual, expected, extra_msg_or_nil)1775if actual == expected then1776local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG1777M.PRINT_TABLE_REF_IN_ERROR_MSG = true1778local s_expected1779if not M.ORDER_ACTUAL_EXPECTED then1780s_expected = prettystrPairs(actual)1781else1782s_expected = prettystrPairs(expected)1783end1784M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg1785fail_fmt(2, extra_msg_or_nil, 'expected and actual object should be different: %s', s_expected)1786end1787end178817891790------------------------------------------------------------------1791-- Scientific assertions1792------------------------------------------------------------------179317941795function M.assertIsNaN(value, extra_msg_or_nil)1796if type(value) ~= "number" or value == value then1797failure("expected: NaN, actual: " .. prettystr(value), extra_msg_or_nil, 2)1798end1799end18001801function M.assertNotIsNaN(value, extra_msg_or_nil)1802if type(value) == "number" and value ~= value then1803failure("expected: not NaN, actual: NaN", extra_msg_or_nil, 2)1804end1805end18061807function M.assertIsInf(value, extra_msg_or_nil)1808if type(value) ~= "number" or math.abs(value) ~= math.huge then1809failure("expected: #Inf, actual: " .. prettystr(value), extra_msg_or_nil, 2)1810end1811end18121813function M.assertIsPlusInf(value, extra_msg_or_nil)1814if type(value) ~= "number" or value ~= math.huge then1815failure("expected: #Inf, actual: " .. prettystr(value), extra_msg_or_nil, 2)1816end1817end18181819function M.assertIsMinusInf(value, extra_msg_or_nil)1820if type(value) ~= "number" or value ~= -math.huge then1821failure("expected: -#Inf, actual: " .. prettystr(value), extra_msg_or_nil, 2)1822end1823end18241825function M.assertNotIsPlusInf(value, extra_msg_or_nil)1826if type(value) == "number" and value == math.huge then1827failure("expected: not #Inf, actual: #Inf", extra_msg_or_nil, 2)1828end1829end18301831function M.assertNotIsMinusInf(value, extra_msg_or_nil)1832if type(value) == "number" and value == -math.huge then1833failure("expected: not -#Inf, actual: -#Inf", extra_msg_or_nil, 2)1834end1835end18361837function M.assertNotIsInf(value, extra_msg_or_nil)1838if type(value) == "number" and math.abs(value) == math.huge then1839failure("expected: not infinity, actual: " .. prettystr(value), extra_msg_or_nil, 2)1840end1841end18421843function M.assertIsPlusZero(value, extra_msg_or_nil)1844if type(value) ~= 'number' or value ~= 0 then1845failure("expected: +0.0, actual: " .. prettystr(value), extra_msg_or_nil, 2)1846else1847if (1 / value == -math.huge) then1848-- more precise error diagnosis1849failure("expected: +0.0, actual: -0.0", extra_msg_or_nil, 2)1850else1851if (1 / value ~= math.huge) then1852-- strange, case should have already been covered1853failure("expected: +0.0, actual: " .. prettystr(value), extra_msg_or_nil, 2)1854end1855end1856end1857end18581859function M.assertIsMinusZero(value, extra_msg_or_nil)1860if type(value) ~= 'number' or value ~= 0 then1861failure("expected: -0.0, actual: " .. prettystr(value), extra_msg_or_nil, 2)1862else1863if (1 / value == math.huge) then1864-- more precise error diagnosis1865failure("expected: -0.0, actual: +0.0", extra_msg_or_nil, 2)1866else1867if (1 / value ~= -math.huge) then1868-- strange, case should have already been covered1869failure("expected: -0.0, actual: " .. prettystr(value), extra_msg_or_nil, 2)1870end1871end1872end1873end18741875function M.assertNotIsPlusZero(value, extra_msg_or_nil)1876if type(value) == 'number' and (1 / value == math.huge) then1877failure("expected: not +0.0, actual: +0.0", extra_msg_or_nil, 2)1878end1879end18801881function M.assertNotIsMinusZero(value, extra_msg_or_nil)1882if type(value) == 'number' and (1 / value == -math.huge) then1883failure("expected: not -0.0, actual: -0.0", extra_msg_or_nil, 2)1884end1885end18861887function M.assertTableContains(t, expected, extra_msg_or_nil)1888-- checks that table t contains the expected element1889if table_findkeyof(t, expected) == nil then1890t, expected = prettystrPairs(t, expected)1891fail_fmt(2, extra_msg_or_nil, 'Table %s does NOT contain the expected element %s',1892t, expected)1893end1894end18951896function M.assertNotTableContains(t, expected, extra_msg_or_nil)1897-- checks that table t doesn't contain the expected element1898local k = table_findkeyof(t, expected)1899if k ~= nil then1900t, expected = prettystrPairs(t, expected)1901fail_fmt(2, extra_msg_or_nil, 'Table %s DOES contain the unwanted element %s (at key %s)',1902t, expected, prettystr(k))1903end1904end19051906----------------------------------------------------------------1907-- Compatibility layer1908----------------------------------------------------------------19091910-- for compatibility with LuaUnit v2.x1911function M.wrapFunctions()1912-- In LuaUnit version <= 2.1 , this function was necessary to include1913-- a test function inside the global test suite. Nowadays, the functions1914-- are simply run directly as part of the test discovery process.1915-- so just do nothing !1916io.stderr:write [[Use of WrapFunctions() is no longer needed.1917Just prefix your test function names with "test" or "Test" and they1918will be picked up and run by LuaUnit.1919]]1920end19211922local list_of_funcs = {1923-- { official function name , alias }19241925-- general assertions1926{ 'assertEquals', 'assert_equals' },1927{ 'assertItemsEquals', 'assert_items_equals' },1928{ 'assertNotEquals', 'assert_not_equals' },1929{ 'assertAlmostEquals', 'assert_almost_equals' },1930{ 'assertNotAlmostEquals', 'assert_not_almost_equals' },1931{ 'assertEvalToTrue', 'assert_eval_to_true' },1932{ 'assertEvalToFalse', 'assert_eval_to_false' },1933{ 'assertStrContains', 'assert_str_contains' },1934{ 'assertStrIContains', 'assert_str_icontains' },1935{ 'assertNotStrContains', 'assert_not_str_contains' },1936{ 'assertNotStrIContains', 'assert_not_str_icontains' },1937{ 'assertStrMatches', 'assert_str_matches' },1938{ 'assertError', 'assert_error' },1939{ 'assertErrorMsgEquals', 'assert_error_msg_equals' },1940{ 'assertErrorMsgContains', 'assert_error_msg_contains' },1941{ 'assertErrorMsgMatches', 'assert_error_msg_matches' },1942{ 'assertErrorMsgContentEquals', 'assert_error_msg_content_equals' },1943{ 'assertIs', 'assert_is' },1944{ 'assertNotIs', 'assert_not_is' },1945{ 'assertTableContains', 'assert_table_contains' },1946{ 'assertNotTableContains', 'assert_not_table_contains' },1947{ 'wrapFunctions', 'WrapFunctions' },1948{ 'wrapFunctions', 'wrap_functions' },19491950-- type assertions: assertIsXXX -> assert_is_xxx1951{ 'assertIsNumber', 'assert_is_number' },1952{ 'assertIsString', 'assert_is_string' },1953{ 'assertIsTable', 'assert_is_table' },1954{ 'assertIsBoolean', 'assert_is_boolean' },1955{ 'assertIsNil', 'assert_is_nil' },1956{ 'assertIsTrue', 'assert_is_true' },1957{ 'assertIsFalse', 'assert_is_false' },1958{ 'assertIsNaN', 'assert_is_nan' },1959{ 'assertIsInf', 'assert_is_inf' },1960{ 'assertIsPlusInf', 'assert_is_plus_inf' },1961{ 'assertIsMinusInf', 'assert_is_minus_inf' },1962{ 'assertIsPlusZero', 'assert_is_plus_zero' },1963{ 'assertIsMinusZero', 'assert_is_minus_zero' },1964{ 'assertIsFunction', 'assert_is_function' },1965{ 'assertIsThread', 'assert_is_thread' },1966{ 'assertIsUserdata', 'assert_is_userdata' },19671968-- type assertions: assertIsXXX -> assertXxx1969{ 'assertIsNumber', 'assertNumber' },1970{ 'assertIsString', 'assertString' },1971{ 'assertIsTable', 'assertTable' },1972{ 'assertIsBoolean', 'assertBoolean' },1973{ 'assertIsNil', 'assertNil' },1974{ 'assertIsTrue', 'assertTrue' },1975{ 'assertIsFalse', 'assertFalse' },1976{ 'assertIsNaN', 'assertNaN' },1977{ 'assertIsInf', 'assertInf' },1978{ 'assertIsPlusInf', 'assertPlusInf' },1979{ 'assertIsMinusInf', 'assertMinusInf' },1980{ 'assertIsPlusZero', 'assertPlusZero' },1981{ 'assertIsMinusZero', 'assertMinusZero' },1982{ 'assertIsFunction', 'assertFunction' },1983{ 'assertIsThread', 'assertThread' },1984{ 'assertIsUserdata', 'assertUserdata' },19851986-- type assertions: assertIsXXX -> assert_xxx (luaunit v2 compat)1987{ 'assertIsNumber', 'assert_number' },1988{ 'assertIsString', 'assert_string' },1989{ 'assertIsTable', 'assert_table' },1990{ 'assertIsBoolean', 'assert_boolean' },1991{ 'assertIsNil', 'assert_nil' },1992{ 'assertIsTrue', 'assert_true' },1993{ 'assertIsFalse', 'assert_false' },1994{ 'assertIsNaN', 'assert_nan' },1995{ 'assertIsInf', 'assert_inf' },1996{ 'assertIsPlusInf', 'assert_plus_inf' },1997{ 'assertIsMinusInf', 'assert_minus_inf' },1998{ 'assertIsPlusZero', 'assert_plus_zero' },1999{ 'assertIsMinusZero', 'assert_minus_zero' },2000{ 'assertIsFunction', 'assert_function' },2001{ 'assertIsThread', 'assert_thread' },2002{ 'assertIsUserdata', 'assert_userdata' },20032004-- type assertions: assertNotIsXXX -> assert_not_is_xxx2005{ 'assertNotIsNumber', 'assert_not_is_number' },2006{ 'assertNotIsString', 'assert_not_is_string' },2007{ 'assertNotIsTable', 'assert_not_is_table' },2008{ 'assertNotIsBoolean', 'assert_not_is_boolean' },2009{ 'assertNotIsNil', 'assert_not_is_nil' },2010{ 'assertNotIsTrue', 'assert_not_is_true' },2011{ 'assertNotIsFalse', 'assert_not_is_false' },2012{ 'assertNotIsNaN', 'assert_not_is_nan' },2013{ 'assertNotIsInf', 'assert_not_is_inf' },2014{ 'assertNotIsPlusInf', 'assert_not_plus_inf' },2015{ 'assertNotIsMinusInf', 'assert_not_minus_inf' },2016{ 'assertNotIsPlusZero', 'assert_not_plus_zero' },2017{ 'assertNotIsMinusZero', 'assert_not_minus_zero' },2018{ 'assertNotIsFunction', 'assert_not_is_function' },2019{ 'assertNotIsThread', 'assert_not_is_thread' },2020{ 'assertNotIsUserdata', 'assert_not_is_userdata' },20212022-- type assertions: assertNotIsXXX -> assertNotXxx (luaunit v2 compat)2023{ 'assertNotIsNumber', 'assertNotNumber' },2024{ 'assertNotIsString', 'assertNotString' },2025{ 'assertNotIsTable', 'assertNotTable' },2026{ 'assertNotIsBoolean', 'assertNotBoolean' },2027{ 'assertNotIsNil', 'assertNotNil' },2028{ 'assertNotIsTrue', 'assertNotTrue' },2029{ 'assertNotIsFalse', 'assertNotFalse' },2030{ 'assertNotIsNaN', 'assertNotNaN' },2031{ 'assertNotIsInf', 'assertNotInf' },2032{ 'assertNotIsPlusInf', 'assertNotPlusInf' },2033{ 'assertNotIsMinusInf', 'assertNotMinusInf' },2034{ 'assertNotIsPlusZero', 'assertNotPlusZero' },2035{ 'assertNotIsMinusZero', 'assertNotMinusZero' },2036{ 'assertNotIsFunction', 'assertNotFunction' },2037{ 'assertNotIsThread', 'assertNotThread' },2038{ 'assertNotIsUserdata', 'assertNotUserdata' },20392040-- type assertions: assertNotIsXXX -> assert_not_xxx2041{ 'assertNotIsNumber', 'assert_not_number' },2042{ 'assertNotIsString', 'assert_not_string' },2043{ 'assertNotIsTable', 'assert_not_table' },2044{ 'assertNotIsBoolean', 'assert_not_boolean' },2045{ 'assertNotIsNil', 'assert_not_nil' },2046{ 'assertNotIsTrue', 'assert_not_true' },2047{ 'assertNotIsFalse', 'assert_not_false' },2048{ 'assertNotIsNaN', 'assert_not_nan' },2049{ 'assertNotIsInf', 'assert_not_inf' },2050{ 'assertNotIsPlusInf', 'assert_not_plus_inf' },2051{ 'assertNotIsMinusInf', 'assert_not_minus_inf' },2052{ 'assertNotIsPlusZero', 'assert_not_plus_zero' },2053{ 'assertNotIsMinusZero', 'assert_not_minus_zero' },2054{ 'assertNotIsFunction', 'assert_not_function' },2055{ 'assertNotIsThread', 'assert_not_thread' },2056{ 'assertNotIsUserdata', 'assert_not_userdata' },20572058-- all assertions with Coroutine duplicate Thread assertions2059{ 'assertIsThread', 'assertIsCoroutine' },2060{ 'assertIsThread', 'assertCoroutine' },2061{ 'assertIsThread', 'assert_is_coroutine' },2062{ 'assertIsThread', 'assert_coroutine' },2063{ 'assertNotIsThread', 'assertNotIsCoroutine' },2064{ 'assertNotIsThread', 'assertNotCoroutine' },2065{ 'assertNotIsThread', 'assert_not_is_coroutine' },2066{ 'assertNotIsThread', 'assert_not_coroutine' },2067}20682069-- Create all aliases in M2070for _, v in ipairs(list_of_funcs) do2071local funcname, alias = v[1], v[2]2072M[alias] = M[funcname]20732074if EXPORT_ASSERT_TO_GLOBALS then2075_G[funcname] = M[funcname]2076_G[alias] = M[funcname]2077end2078end20792080----------------------------------------------------------------2081--2082-- Outputters2083--2084----------------------------------------------------------------20852086-- A common "base" class for outputters2087-- For concepts involved (class inheritance) see http://www.lua.org/pil/16.2.html20882089local genericOutput = { __class__ = 'genericOutput' } -- class2090local genericOutput_MT = { __index = genericOutput } -- metatable2091M.genericOutput = genericOutput -- publish, so that custom classes may derive from it20922093function genericOutput.new(runner, default_verbosity)2094-- runner is the "parent" object controlling the output, usually a LuaUnit instance2095local t = { runner = runner }2096if runner then2097t.result = runner.result2098t.verbosity = runner.verbosity or default_verbosity2099t.fname = runner.fname2100else2101t.verbosity = default_verbosity2102end2103return setmetatable(t, genericOutput_MT)2104end21052106-- abstract ("empty") methods2107function genericOutput:startSuite()2108-- Called once, when the suite is started2109end21102111function genericOutput:startClass(className)2112-- Called each time a new test class is started2113end21142115function genericOutput:startTest(testName)2116-- called each time a new test is started, right before the setUp()2117-- the current test status node is already created and available in: self.result.currentNode2118end21192120function genericOutput:updateStatus(node)2121-- called with status failed or error as soon as the error/failure is encountered2122-- this method is NOT called for a successful test because a test is marked as successful by default2123-- and does not need to be updated2124end21252126function genericOutput:endTest(node)2127-- called when the test is finished, after the tearDown() method2128end21292130function genericOutput:endClass()2131-- called when executing the class is finished, before moving on to the next class of at the end of the test execution2132end21332134function genericOutput:endSuite()2135-- called at the end of the test suite execution2136end213721382139----------------------------------------------------------------2140-- class TapOutput2141----------------------------------------------------------------21422143local TapOutput = genericOutput.new() -- derived class2144local TapOutput_MT = { __index = TapOutput } -- metatable2145TapOutput.__class__ = 'TapOutput'21462147-- For a good reference for TAP format, check: http://testanything.org/tap-specification.html21482149function TapOutput.new(runner)2150local t = genericOutput.new(runner, M.VERBOSITY_LOW)2151return setmetatable(t, TapOutput_MT)2152end2153function TapOutput:startSuite()2154print("1.." .. self.result.selectedCount)2155print('# Started on ' .. self.result.startDate)2156end2157function TapOutput:startClass(className)2158if className ~= '[TestFunctions]' then2159print('# Starting class: ' .. className)2160end2161end21622163function TapOutput:updateStatus(node)2164if node:isSkipped() then2165io.stdout:write("ok ", self.result.currentTestNumber, "\t# SKIP ", node.msg, "\n")2166return2167end21682169io.stdout:write("not ok ", self.result.currentTestNumber, "\t", node.testName, "\n")2170if self.verbosity > M.VERBOSITY_LOW then2171print(prefixString('# ', node.msg))2172end2173if (node:isFailure() or node:isError()) and self.verbosity > M.VERBOSITY_DEFAULT then2174print(prefixString('# ', node.stackTrace))2175end2176end21772178function TapOutput:endTest(node)2179if node:isSuccess() then2180io.stdout:write("ok ", self.result.currentTestNumber, "\t", node.testName, "\n")2181end2182end21832184function TapOutput:endSuite()2185print('# ' .. M.LuaUnit.statusLine(self.result))2186return self.result.notSuccessCount2187end218821892190-- class TapOutput end21912192----------------------------------------------------------------2193-- class JUnitOutput2194----------------------------------------------------------------21952196-- See directory junitxml for more information about the junit format2197local JUnitOutput = genericOutput.new() -- derived class2198local JUnitOutput_MT = { __index = JUnitOutput } -- metatable2199JUnitOutput.__class__ = 'JUnitOutput'22002201function JUnitOutput.new(runner)2202local t = genericOutput.new(runner, M.VERBOSITY_LOW)2203t.testList = {}2204return setmetatable(t, JUnitOutput_MT)2205end22062207function JUnitOutput:startSuite()2208-- open xml file early to deal with errors2209if self.fname == nil then2210error('With Junit, an output filename must be supplied with --name!')2211end2212if string.sub(self.fname, -4) ~= '.xml' then2213self.fname = self.fname .. '.xml'2214end2215self.fd = io.open(self.fname, "w")2216if self.fd == nil then2217error("Could not open file for writing: " .. self.fname)2218end22192220print('# XML output to ' .. self.fname)2221print('# Started on ' .. self.result.startDate)2222end2223function JUnitOutput:startClass(className)2224if className ~= '[TestFunctions]' then2225print('# Starting class: ' .. className)2226end2227end2228function JUnitOutput:startTest(testName)2229print('# Starting test: ' .. testName)2230end22312232function JUnitOutput:updateStatus(node)2233if node:isFailure() then2234print('# Failure: ' .. prefixString('# ', node.msg):sub(4, nil))2235-- print('# ' .. node.stackTrace)2236elseif node:isError() then2237print('# Error: ' .. prefixString('# ', node.msg):sub(4, nil))2238-- print('# ' .. node.stackTrace)2239end2240end22412242function JUnitOutput:endSuite()2243print('# ' .. M.LuaUnit.statusLine(self.result))22442245-- XML file writing2246self.fd:write('<?xml version="1.0" encoding="UTF-8" ?>\n')2247self.fd:write('<testsuites>\n')2248self.fd:write(string.format(2249' <testsuite name="LuaUnit" id="00001" package="" hostname="localhost" tests="%d" timestamp="%s" time="%0.3f" errors="%d" failures="%d" skipped="%d">\n',2250self.result.runCount, self.result.startIsodate, self.result.duration, self.result.errorCount, self.result.failureCount, self.result.skippedCount))2251self.fd:write(" <properties>\n")2252self.fd:write(string.format(' <property name="Lua Version" value="%s"/>\n', _VERSION))2253self.fd:write(string.format(' <property name="LuaUnit Version" value="%s"/>\n', M.VERSION))2254-- XXX please include system name and version if possible2255self.fd:write(" </properties>\n")22562257for i, node in ipairs(self.result.allTests) do2258self.fd:write(string.format(' <testcase classname="%s" name="%s" time="%0.3f">\n',2259node.className, node.testName, node.duration))2260if node:isNotSuccess() then2261self.fd:write(node:statusXML())2262end2263self.fd:write(' </testcase>\n')2264end22652266-- Next two lines are needed to validate junit ANT xsd, but really not useful in general:2267self.fd:write(' <system-out/>\n')2268self.fd:write(' <system-err/>\n')22692270self.fd:write(' </testsuite>\n')2271self.fd:write('</testsuites>\n')2272self.fd:close()2273return self.result.notSuccessCount2274end227522762277-- class TapOutput end22782279----------------------------------------------------------------2280-- class TextOutput2281----------------------------------------------------------------22822283--[[ Example of other unit-tests suite text output22842285-- Python Non verbose:22862287For each test: . or F or E22882289If some failed tests:2290==============2291ERROR / FAILURE: TestName (testfile.testclass)2292---------2293Stack trace229422952296then --------------2297then "Ran x tests in 0.000s"2298then OK or FAILED (failures=1, error=1)22992300-- Python Verbose:2301testname (filename.classname) ... ok2302testname (filename.classname) ... FAIL2303testname (filename.classname) ... ERROR23042305then --------------2306then "Ran x tests in 0.000s"2307then OK or FAILED (failures=1, error=1)23082309-- Ruby:2310Started2311.2312Finished in 0.002695 seconds.231323141 tests, 2 assertions, 0 failures, 0 errors23152316-- Ruby:2317>> ruby tc_simple_number2.rb2318Loaded suite tc_simple_number22319Started2320F..2321Finished in 0.038617 seconds.232223231) Failure:2324test_failure(TestSimpleNumber) [tc_simple_number2.rb:16]:2325Adding doesn't work.2326<3> expected but was2327<4>.232823293 tests, 4 assertions, 1 failures, 0 errors23302331-- Java Junit2332.......F.2333Time: 0,0032334There was 1 failure:23351) testCapacity(junit.samples.VectorTest)junit.framework.AssertionFailedError2336at junit.samples.VectorTest.testCapacity(VectorTest.java:87)2337at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)2338at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)2339at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)23402341FAILURES!!!2342Tests run: 8, Failures: 1, Errors: 0234323442345-- Maven23462347# mvn test2348-------------------------------------------------------2349T E S T S2350-------------------------------------------------------2351Running math.AdditionTest2352Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed:23530.03 sec <<< FAILURE!23542355Results :23562357Failed tests:2358testLireSymbole(math.AdditionTest)23592360Tests run: 2, Failures: 1, Errors: 0, Skipped: 0236123622363-- LuaUnit2364---- non verbose2365* display . or F or E when running tests2366---- verbose2367* display test name + ok/fail2368----2369* blank line2370* number) ERROR or FAILURE: TestName2371Stack trace2372* blank line2373* number) ERROR or FAILURE: TestName2374Stack trace23752376then --------------2377then "Ran x tests in 0.000s (%d not selected, %d skipped)"2378then OK or FAILED (failures=1, error=1)237923802381]]23822383local TextOutput = genericOutput.new() -- derived class2384local TextOutput_MT = { __index = TextOutput } -- metatable2385TextOutput.__class__ = 'TextOutput'23862387function TextOutput.new(runner)2388local t = genericOutput.new(runner, M.VERBOSITY_DEFAULT)2389t.errorList = {}2390return setmetatable(t, TextOutput_MT)2391end23922393function TextOutput:startSuite()2394if self.verbosity > M.VERBOSITY_DEFAULT then2395print('Started on ' .. self.result.startDate)2396end2397end23982399function TextOutput:startTest(testName)2400if self.verbosity > M.VERBOSITY_DEFAULT then2401io.stdout:write(" ", self.result.currentNode.testName, " ... ")2402end2403end24042405function TextOutput:endTest(node)2406if node:isSuccess() then2407if self.verbosity > M.VERBOSITY_DEFAULT then2408io.stdout:write("Ok\n")2409else2410io.stdout:write(".")2411io.stdout:flush()2412end2413else2414if self.verbosity > M.VERBOSITY_DEFAULT then2415print(node.status)2416print(node.msg)2417--[[2418-- find out when to do this:2419if self.verbosity > M.VERBOSITY_DEFAULT then2420print( node.stackTrace )2421end2422]]2423else2424-- write only the first character of status E, F or S2425io.stdout:write(string.sub(node.status, 1, 1))2426io.stdout:flush()2427end2428end2429end24302431function TextOutput:displayOneFailedTest(index, fail)2432print(index .. ") " .. fail.testName)2433print(fail.msg)2434print(fail.stackTrace)2435print()2436end24372438function TextOutput:displayErroredTests()2439if #self.result.errorTests ~= 0 then2440print("Tests with errors:")2441print("------------------")2442for i, v in ipairs(self.result.errorTests) do2443self:displayOneFailedTest(i, v)2444end2445end2446end24472448function TextOutput:displayFailedTests()2449if #self.result.failedTests ~= 0 then2450print("Failed tests:")2451print("-------------")2452for i, v in ipairs(self.result.failedTests) do2453self:displayOneFailedTest(i, v)2454end2455end2456end24572458function TextOutput:endSuite()2459if self.verbosity > M.VERBOSITY_DEFAULT then2460print("=========================================================")2461else2462print()2463end2464self:displayErroredTests()2465self:displayFailedTests()2466print(M.LuaUnit.statusLine(self.result))2467if self.result.notSuccessCount == 0 then2468print('OK')2469end2470end24712472-- class TextOutput end247324742475----------------------------------------------------------------2476-- class NilOutput2477----------------------------------------------------------------24782479local function nopCallable()2480--print(42)2481return nopCallable2482end24832484local NilOutput = { __class__ = 'NilOuptut' } -- class2485local NilOutput_MT = { __index = nopCallable } -- metatable24862487function NilOutput.new(runner)2488return setmetatable({ __class__ = 'NilOutput' }, NilOutput_MT)2489end24902491----------------------------------------------------------------2492--2493-- class LuaUnit2494--2495----------------------------------------------------------------24962497M.LuaUnit = {2498outputType = TextOutput,2499verbosity = M.VERBOSITY_DEFAULT,2500__class__ = 'LuaUnit',2501instances = {}2502}2503local LuaUnit_MT = { __index = M.LuaUnit }25042505if EXPORT_ASSERT_TO_GLOBALS then2506LuaUnit = M.LuaUnit2507end25082509function M.LuaUnit.new()2510local newInstance = setmetatable({}, LuaUnit_MT)2511return newInstance2512end25132514-----------------[[ Utility methods ]]---------------------25152516function M.LuaUnit.asFunction(aObject)2517-- return "aObject" if it is a function, and nil otherwise2518if 'function' == type(aObject) then2519return aObject2520end2521end25222523function M.LuaUnit.splitClassMethod(someName)2524--[[2525Return a pair of className, methodName strings for a name in the form2526"class.method". If no class part (or separator) is found, will return2527nil, someName instead (the latter being unchanged).25282529This convention thus also replaces the older isClassMethod() test:2530You just have to check for a non-nil className (return) value.2531]]2532local separator = string.find(someName, '.', 1, true)2533if separator then2534return someName:sub(1, separator - 1), someName:sub(separator + 1)2535end2536return nil, someName2537end25382539function M.LuaUnit.isMethodTestName(s)2540-- return true is the name matches the name of a test method2541-- default rule is that is starts with 'Test' or with 'test'2542return string.sub(s, 1, 4):lower() == 'test'2543end25442545function M.LuaUnit.isTestName(s)2546-- return true is the name matches the name of a test2547-- default rule is that is starts with 'Test' or with 'test'2548return string.sub(s, 1, 4):lower() == 'test'2549end25502551function M.LuaUnit.collectTests()2552-- return a list of all test names in the global namespace2553-- that match LuaUnit.isTestName25542555local testNames = {}2556for k, _ in pairs(_G) do2557if type(k) == "string" and M.LuaUnit.isTestName(k) then2558table.insert(testNames, k)2559end2560end2561table.sort(testNames)2562return testNames2563end25642565function M.LuaUnit.parseCmdLine(cmdLine)2566-- parse the command line2567-- Supported command line parameters:2568-- --verbose, -v: increase verbosity2569-- --quiet, -q: silence output2570-- --error, -e: treat errors as fatal (quit program)2571-- --output, -o, + name: select output type2572-- --pattern, -p, + pattern: run test matching pattern, may be repeated2573-- --exclude, -x, + pattern: run test not matching pattern, may be repeated2574-- --shuffle, -s, : shuffle tests before reunning them2575-- --name, -n, + fname: name of output file for junit, default to stdout2576-- --repeat, -r, + num: number of times to execute each test2577-- [testnames, ...]: run selected test names2578--2579-- Returns a table with the following fields:2580-- verbosity: nil, M.VERBOSITY_DEFAULT, M.VERBOSITY_QUIET, M.VERBOSITY_VERBOSE2581-- output: nil, 'tap', 'junit', 'text', 'nil'2582-- testNames: nil or a list of test names to run2583-- exeRepeat: num or 12584-- pattern: nil or a list of patterns2585-- exclude: nil or a list of patterns25862587local result, state = {}, nil2588local SET_OUTPUT = 12589local SET_PATTERN = 22590local SET_EXCLUDE = 32591local SET_FNAME = 42592local SET_REPEAT = 525932594if cmdLine == nil then2595return result2596end25972598local function parseOption(option)2599if option == '--help' or option == '-h' then2600result['help'] = true2601return2602elseif option == '--version' then2603result['version'] = true2604return2605elseif option == '--verbose' or option == '-v' then2606result['verbosity'] = M.VERBOSITY_VERBOSE2607return2608elseif option == '--quiet' or option == '-q' then2609result['verbosity'] = M.VERBOSITY_QUIET2610return2611elseif option == '--error' or option == '-e' then2612result['quitOnError'] = true2613return2614elseif option == '--failure' or option == '-f' then2615result['quitOnFailure'] = true2616return2617elseif option == '--shuffle' or option == '-s' then2618result['shuffle'] = true2619return2620elseif option == '--output' or option == '-o' then2621state = SET_OUTPUT2622return state2623elseif option == '--name' or option == '-n' then2624state = SET_FNAME2625return state2626elseif option == '--repeat' or option == '-r' then2627state = SET_REPEAT2628return state2629elseif option == '--pattern' or option == '-p' then2630state = SET_PATTERN2631return state2632elseif option == '--exclude' or option == '-x' then2633state = SET_EXCLUDE2634return state2635end2636error('Unknown option: ' .. option, 3)2637end26382639local function setArg(cmdArg, state)2640if state == SET_OUTPUT then2641result['output'] = cmdArg2642return2643elseif state == SET_FNAME then2644result['fname'] = cmdArg2645return2646elseif state == SET_REPEAT then2647result['exeRepeat'] = tonumber(cmdArg)2648or error('Malformed -r argument: ' .. cmdArg)2649return2650elseif state == SET_PATTERN then2651if result['pattern'] then2652table.insert(result['pattern'], cmdArg)2653else2654result['pattern'] = { cmdArg }2655end2656return2657elseif state == SET_EXCLUDE then2658local notArg = '!' .. cmdArg2659if result['pattern'] then2660table.insert(result['pattern'], notArg)2661else2662result['pattern'] = { notArg }2663end2664return2665end2666error('Unknown parse state: ' .. state)2667end26682669for i, cmdArg in ipairs(cmdLine) do2670if state ~= nil then2671setArg(cmdArg, state, result)2672state = nil2673else2674if cmdArg:sub(1, 1) == '-' then2675state = parseOption(cmdArg)2676else2677if result['testNames'] then2678table.insert(result['testNames'], cmdArg)2679else2680result['testNames'] = { cmdArg }2681end2682end2683end2684end26852686if result['help'] then2687M.LuaUnit.help()2688end26892690if result['version'] then2691M.LuaUnit.version()2692end26932694if state ~= nil then2695error('Missing argument after ' .. cmdLine[#cmdLine], 2)2696end26972698return result2699end27002701function M.LuaUnit.help()2702print(M.USAGE)2703os.exit(0)2704end27052706function M.LuaUnit.version()2707print('LuaUnit v' .. M.VERSION .. ' by Philippe Fremy <[email protected]>')2708os.exit(0)2709end27102711----------------------------------------------------------------2712-- class NodeStatus2713----------------------------------------------------------------27142715local NodeStatus = { __class__ = 'NodeStatus' } -- class2716local NodeStatus_MT = { __index = NodeStatus } -- metatable2717M.NodeStatus = NodeStatus27182719-- values of status2720NodeStatus.SUCCESS = 'SUCCESS'2721NodeStatus.SKIP = 'SKIP'2722NodeStatus.FAIL = 'FAIL'2723NodeStatus.ERROR = 'ERROR'27242725function NodeStatus.new(number, testName, className)2726-- default constructor, test are PASS by default2727local t = { number = number, testName = testName, className = className }2728setmetatable(t, NodeStatus_MT)2729t:success()2730return t2731end27322733function NodeStatus:success()2734self.status = self.SUCCESS2735-- useless because lua does this for us, but it helps me remembering the relevant field names2736self.msg = nil2737self.stackTrace = nil2738end27392740function NodeStatus:skip(msg)2741self.status = self.SKIP2742self.msg = msg2743self.stackTrace = nil2744end27452746function NodeStatus:fail(msg, stackTrace)2747self.status = self.FAIL2748self.msg = msg2749self.stackTrace = stackTrace2750end27512752function NodeStatus:error(msg, stackTrace)2753self.status = self.ERROR2754self.msg = msg2755self.stackTrace = stackTrace2756end27572758function NodeStatus:isSuccess()2759return self.status == NodeStatus.SUCCESS2760end27612762function NodeStatus:isNotSuccess()2763-- Return true if node is either failure or error or skip2764return (self.status == NodeStatus.FAIL or self.status == NodeStatus.ERROR or self.status == NodeStatus.SKIP)2765end27662767function NodeStatus:isSkipped()2768return self.status == NodeStatus.SKIP2769end27702771function NodeStatus:isFailure()2772return self.status == NodeStatus.FAIL2773end27742775function NodeStatus:isError()2776return self.status == NodeStatus.ERROR2777end27782779function NodeStatus:statusXML()2780if self:isError() then2781return table.concat(2782{ ' <error type="', xmlEscape(self.msg), '">\n',2783' <![CDATA[', xmlCDataEscape(self.stackTrace),2784']]></error>\n' })2785elseif self:isFailure() then2786return table.concat(2787{ ' <failure type="', xmlEscape(self.msg), '">\n',2788' <![CDATA[', xmlCDataEscape(self.stackTrace),2789']]></failure>\n' })2790elseif self:isSkipped() then2791return table.concat({ ' <skipped>', xmlEscape(self.msg), '</skipped>\n' })2792end2793return ' <passed/>\n' -- (not XSD-compliant! normally shouldn't get here)2794end27952796--------------[[ Output methods ]]-------------------------27972798local function conditional_plural(number, singular)2799-- returns a grammatically well-formed string "%d <singular/plural>"2800local suffix = ''2801if number ~= 1 then2802-- use plural2803suffix = (singular:sub(-2) == 'ss') and 'es' or 's'2804end2805return string.format('%d %s%s', number, singular, suffix)2806end28072808function M.LuaUnit.statusLine(result)2809-- return status line string according to results2810local s = {2811string.format('Ran %d tests in %0.3f seconds',2812result.runCount, result.duration),2813conditional_plural(result.successCount, 'success'),2814}2815if result.notSuccessCount > 0 then2816if result.failureCount > 0 then2817table.insert(s, conditional_plural(result.failureCount, 'failure'))2818end2819if result.errorCount > 0 then2820table.insert(s, conditional_plural(result.errorCount, 'error'))2821end2822else2823table.insert(s, '0 failures')2824end2825if result.skippedCount > 0 then2826table.insert(s, string.format("%d skipped", result.skippedCount))2827end2828if result.nonSelectedCount > 0 then2829table.insert(s, string.format("%d non-selected", result.nonSelectedCount))2830end2831return table.concat(s, ', ')2832end28332834function M.LuaUnit:startSuite(selectedCount, nonSelectedCount)2835self.result = {2836selectedCount = selectedCount,2837nonSelectedCount = nonSelectedCount,2838successCount = 0,2839runCount = 0,2840currentTestNumber = 0,2841currentClassName = "",2842currentNode = nil,2843suiteStarted = true,2844startTime = os.clock(),2845startDate = os.date(os.getenv('LUAUNIT_DATEFMT')),2846startIsodate = os.date('%Y-%m-%dT%H:%M:%S'),2847patternIncludeFilter = self.patternIncludeFilter,28482849-- list of test node status2850allTests = {},2851failedTests = {},2852errorTests = {},2853skippedTests = {},28542855failureCount = 0,2856errorCount = 0,2857notSuccessCount = 0,2858skippedCount = 0,2859}28602861self.outputType = self.outputType or TextOutput2862self.output = self.outputType.new(self)2863self.output:startSuite()2864end28652866function M.LuaUnit:startClass(className, classInstance)2867self.result.currentClassName = className2868self.output:startClass(className)2869self:setupClass(className, classInstance)2870end28712872function M.LuaUnit:startTest(testName)2873self.result.currentTestNumber = self.result.currentTestNumber + 12874self.result.runCount = self.result.runCount + 12875self.result.currentNode = NodeStatus.new(2876self.result.currentTestNumber,2877testName,2878self.result.currentClassName2879)2880self.result.currentNode.startTime = os.clock()2881table.insert(self.result.allTests, self.result.currentNode)2882self.output:startTest(testName)2883end28842885function M.LuaUnit:updateStatus(err)2886-- "err" is expected to be a table / result from protectedCall()2887if err.status == NodeStatus.SUCCESS then2888return2889end28902891local node = self.result.currentNode28922893--[[ As a first approach, we will report only one error or one failure for one test.28942895However, we can have the case where the test is in failure, and the teardown is in error.2896In such case, it's a good idea to report both a failure and an error in the test suite. This is2897what Python unittest does for example. However, it mixes up counts so need to be handled carefully: for2898example, there could be more (failures + errors) count that tests. What happens to the current node ?28992900We will do this more intelligent version later.2901]]29022903-- if the node is already in failure/error, just don't report the new error (see above)2904if node.status ~= NodeStatus.SUCCESS then2905return2906end29072908if err.status == NodeStatus.FAIL then2909node:fail(err.msg, err.trace)2910table.insert(self.result.failedTests, node)2911elseif err.status == NodeStatus.ERROR then2912node:error(err.msg, err.trace)2913table.insert(self.result.errorTests, node)2914elseif err.status == NodeStatus.SKIP then2915node:skip(err.msg)2916table.insert(self.result.skippedTests, node)2917else2918error('No such status: ' .. prettystr(err.status))2919end29202921self.output:updateStatus(node)2922end29232924function M.LuaUnit:endTest()2925local node = self.result.currentNode2926-- print( 'endTest() '..prettystr(node))2927-- print( 'endTest() '..prettystr(node:isNotSuccess()))2928node.duration = os.clock() - node.startTime2929node.startTime = nil2930self.output:endTest(node)29312932if node:isSuccess() then2933self.result.successCount = self.result.successCount + 12934elseif node:isError() then2935if self.quitOnError or self.quitOnFailure then2936-- Runtime error - abort test execution as requested by2937-- "--error" option. This is done by setting a special2938-- flag that gets handled in internalRunSuiteByInstances().2939print("\nERROR during LuaUnit test execution:\n" .. node.msg)2940self.result.aborted = true2941end2942elseif node:isFailure() then2943if self.quitOnFailure then2944-- Failure - abort test execution as requested by2945-- "--failure" option. This is done by setting a special2946-- flag that gets handled in internalRunSuiteByInstances().2947print("\nFailure during LuaUnit test execution:\n" .. node.msg)2948self.result.aborted = true2949end2950elseif node:isSkipped() then2951self.result.runCount = self.result.runCount - 12952else2953error('No such node status: ' .. prettystr(node.status))2954end2955self.result.currentNode = nil2956end29572958function M.LuaUnit:endClass()2959self:teardownClass(self.lastClassName, self.lastClassInstance)2960self.output:endClass()2961end29622963function M.LuaUnit:endSuite()2964if self.result.suiteStarted == false then2965error('LuaUnit:endSuite() -- suite was already ended')2966end2967self.result.duration = os.clock() - self.result.startTime2968self.result.suiteStarted = false29692970-- Expose test counts for outputter's endSuite(). This could be managed2971-- internally instead by using the length of the lists of failed tests2972-- but unit tests rely on these fields being present.2973self.result.failureCount = #self.result.failedTests2974self.result.errorCount = #self.result.errorTests2975self.result.notSuccessCount = self.result.failureCount + self.result.errorCount2976self.result.skippedCount = #self.result.skippedTests29772978self.output:endSuite()2979end29802981function M.LuaUnit:setOutputType(outputType, fname)2982-- Configures LuaUnit runner output2983-- outputType is one of: NIL, TAP, JUNIT, TEXT2984-- when outputType is junit, the additional argument fname is used to set the name of junit output file2985-- for other formats, fname is ignored2986if outputType:upper() == "NIL" then2987self.outputType = NilOutput2988return2989end2990if outputType:upper() == "TAP" then2991self.outputType = TapOutput2992return2993end2994if outputType:upper() == "JUNIT" then2995self.outputType = JUnitOutput2996if fname then2997self.fname = fname2998end2999return3000end3001if outputType:upper() == "TEXT" then3002self.outputType = TextOutput3003return3004end3005error('No such format: ' .. outputType, 2)3006end30073008--------------[[ Runner ]]-----------------30093010function M.LuaUnit:protectedCall(classInstance, methodInstance, prettyFuncName)3011-- if classInstance is nil, this is just a function call3012-- else, it's method of a class being called.30133014local function err_handler(e)3015-- transform error into a table, adding the traceback information3016return {3017status = NodeStatus.ERROR,3018msg = e,3019trace = string.sub(debug.traceback("", 1), 2)3020}3021end30223023local ok, err3024if classInstance then3025-- stupid Lua < 5.2 does not allow xpcall with arguments so let's use a workaround3026ok, err = xpcall(function()3027methodInstance(classInstance)3028end, err_handler)3029else3030ok, err = xpcall(function()3031methodInstance()3032end, err_handler)3033end3034if ok then3035return { status = NodeStatus.SUCCESS }3036end3037-- print('ok="'..prettystr(ok)..'" err="'..prettystr(err)..'"')30383039local iter_msg3040iter_msg = self.exeRepeat and 'iteration ' .. self.currentCount30413042err.msg, err.status = M.adjust_err_msg_with_iter(err.msg, iter_msg)30433044if err.status == NodeStatus.SUCCESS or err.status == NodeStatus.SKIP then3045err.trace = nil3046return err3047end30483049-- reformat / improve the stack trace3050if prettyFuncName then3051-- we do have the real method name3052err.trace = err.trace:gsub("in (%a+) 'methodInstance'", "in %1 '" .. prettyFuncName .. "'")3053end3054if STRIP_LUAUNIT_FROM_STACKTRACE then3055err.trace = stripLuaunitTrace2(err.trace, err.msg)3056end30573058return err -- return the error "object" (table)3059end30603061function M.LuaUnit:execOneFunction(className, methodName, classInstance, methodInstance)3062-- When executing a test function, className and classInstance must be nil3063-- When executing a class method, all parameters must be set30643065if type(methodInstance) ~= 'function' then3066self:unregisterSuite()3067error(tostring(methodName) .. ' must be a function, not ' .. type(methodInstance))3068end30693070local prettyFuncName3071if className == nil then3072className = '[TestFunctions]'3073prettyFuncName = methodName3074else3075prettyFuncName = className .. '.' .. methodName3076end30773078if self.lastClassName ~= className then3079if self.lastClassName ~= nil then3080self:endClass()3081end3082self:startClass(className, classInstance)3083self.lastClassName = className3084self.lastClassInstance = classInstance3085end30863087self:startTest(prettyFuncName)30883089local node = self.result.currentNode3090for iter_n = 1, self.exeRepeat or 1 do3091if node:isNotSuccess() then3092break3093end3094self.currentCount = iter_n30953096-- run setUp first (if any)3097if classInstance then3098local func = self.asFunction(classInstance.setUp) or3099self.asFunction(classInstance.Setup) or3100self.asFunction(classInstance.setup) or3101self.asFunction(classInstance.SetUp)3102if func then3103self:updateStatus(self:protectedCall(classInstance, func, className .. '.setUp'))3104end3105end31063107-- run testMethod()3108if node:isSuccess() then3109self:updateStatus(self:protectedCall(classInstance, methodInstance, prettyFuncName))3110end31113112-- lastly, run tearDown (if any)3113if classInstance then3114local func = self.asFunction(classInstance.tearDown) or3115self.asFunction(classInstance.TearDown) or3116self.asFunction(classInstance.teardown) or3117self.asFunction(classInstance.Teardown)3118if func then3119self:updateStatus(self:protectedCall(classInstance, func, className .. '.tearDown'))3120end3121end3122end31233124self:endTest()3125end31263127function M.LuaUnit.expandOneClass(result, className, classInstance)3128--[[3129Input: a list of { name, instance }, a class name, a class instance3130Ouptut: modify result to add all test method instance in the form:3131{ className.methodName, classInstance }3132]]3133for methodName, methodInstance in sortedPairs(classInstance) do3134if M.LuaUnit.asFunction(methodInstance) and M.LuaUnit.isMethodTestName(methodName) then3135table.insert(result, { className .. '.' .. methodName, classInstance })3136end3137end3138end31393140function M.LuaUnit.expandClasses(listOfNameAndInst)3141--[[3142-- expand all classes (provided as {className, classInstance}) to a list of {className.methodName, classInstance}3143-- functions and methods remain untouched31443145Input: a list of { name, instance }31463147Output:3148* { function name, function instance } : do nothing3149* { class.method name, class instance }: do nothing3150* { class name, class instance } : add all method names in the form of (className.methodName, classInstance)3151]]3152local result = {}31533154for i, v in ipairs(listOfNameAndInst) do3155local name, instance = v[1], v[2]3156if M.LuaUnit.asFunction(instance) then3157table.insert(result, { name, instance })3158else3159if type(instance) ~= 'table' then3160error('Instance must be a table or a function, not a ' .. type(instance) .. ' with value ' .. prettystr(instance))3161end3162local className, methodName = M.LuaUnit.splitClassMethod(name)3163if className then3164local methodInstance = instance[methodName]3165if methodInstance == nil then3166error("Could not find method in class " .. tostring(className) .. " for method " .. tostring(methodName))3167end3168table.insert(result, { name, instance })3169else3170M.LuaUnit.expandOneClass(result, name, instance)3171end3172end3173end31743175return result3176end31773178function M.LuaUnit.applyPatternFilter(patternIncFilter, listOfNameAndInst)3179local included, excluded = {}, {}3180for i, v in ipairs(listOfNameAndInst) do3181-- local name, instance = v[1], v[2]3182if patternFilter(patternIncFilter, v[1]) then3183table.insert(included, v)3184else3185table.insert(excluded, v)3186end3187end3188return included, excluded3189end31903191local function getKeyInListWithGlobalFallback(key, listOfNameAndInst)3192local result = nil3193for i, v in ipairs(listOfNameAndInst) do3194if (listOfNameAndInst[i][1] == key) then3195result = listOfNameAndInst[i][2]3196break3197end3198end3199if (not M.LuaUnit.asFunction(result)) then3200result = _G[key]3201end3202return result3203end32043205function M.LuaUnit:setupSuite(listOfNameAndInst)3206local setupSuite = getKeyInListWithGlobalFallback("setupSuite", listOfNameAndInst)3207if self.asFunction(setupSuite) then3208self:updateStatus(self:protectedCall(nil, setupSuite, 'setupSuite'))3209end3210end32113212function M.LuaUnit:teardownSuite(listOfNameAndInst)3213local teardownSuite = getKeyInListWithGlobalFallback("teardownSuite", listOfNameAndInst)3214if self.asFunction(teardownSuite) then3215self:updateStatus(self:protectedCall(nil, teardownSuite, 'teardownSuite'))3216end3217end32183219function M.LuaUnit:setupClass(className, instance)3220if type(instance) == 'table' and self.asFunction(instance.setupClass) then3221self:updateStatus(self:protectedCall(instance, instance.setupClass, className .. '.setupClass'))3222end3223end32243225function M.LuaUnit:teardownClass(className, instance)3226if type(instance) == 'table' and self.asFunction(instance.teardownClass) then3227self:updateStatus(self:protectedCall(instance, instance.teardownClass, className .. '.teardownClass'))3228end3229end32303231function M.LuaUnit:internalRunSuiteByInstances(listOfNameAndInst)3232--[[ Run an explicit list of tests. Each item of the list must be one of:3233* { function name, function instance }3234* { class name, class instance }3235* { class.method name, class instance }32363237This function is internal to LuaUnit. The official API to perform this action is runSuiteByInstances()3238]]32393240local expandedList = self.expandClasses(listOfNameAndInst)3241if self.shuffle then3242randomizeTable(expandedList)3243end3244local filteredList, filteredOutList = self.applyPatternFilter(3245self.patternIncludeFilter, expandedList)32463247self:startSuite(#filteredList, #filteredOutList)3248self:setupSuite(listOfNameAndInst)32493250for i, v in ipairs(filteredList) do3251local name, instance = v[1], v[2]3252if M.LuaUnit.asFunction(instance) then3253self:execOneFunction(nil, name, nil, instance)3254else3255-- expandClasses() should have already taken care of sanitizing the input3256assert(type(instance) == 'table')3257local className, methodName = M.LuaUnit.splitClassMethod(name)3258assert(className ~= nil)3259local methodInstance = instance[methodName]3260assert(methodInstance ~= nil)3261self:execOneFunction(className, methodName, instance, methodInstance)3262end3263if self.result.aborted then3264break -- "--error" or "--failure" option triggered3265end3266end32673268if self.lastClassName ~= nil then3269self:endClass()3270end32713272self:teardownSuite(listOfNameAndInst)3273self:endSuite()32743275if self.result.aborted then3276print("LuaUnit ABORTED (as requested by --error or --failure option)")3277self:unregisterSuite()3278os.exit(-2)3279end3280end32813282function M.LuaUnit:internalRunSuiteByNames(listOfName)3283--[[ Run LuaUnit with a list of generic names, coming either from command-line or from global3284namespace analysis. Convert the list into a list of (name, valid instances (table or function))3285and calls internalRunSuiteByInstances.3286]]32873288local instanceName, instance3289local listOfNameAndInst = {}32903291for i, name in ipairs(listOfName) do3292local className, methodName = M.LuaUnit.splitClassMethod(name)3293if className then3294instanceName = className3295instance = _G[instanceName]32963297if instance == nil then3298self:unregisterSuite()3299error("No such name in global space: " .. instanceName)3300end33013302if type(instance) ~= 'table' then3303self:unregisterSuite()3304error('Instance of ' .. instanceName .. ' must be a table, not ' .. type(instance))3305end33063307local methodInstance = instance[methodName]3308if methodInstance == nil then3309self:unregisterSuite()3310error("Could not find method in class " .. tostring(className) .. " for method " .. tostring(methodName))3311end33123313else3314-- for functions and classes3315instanceName = name3316instance = _G[instanceName]3317end33183319if instance == nil then3320self:unregisterSuite()3321error("No such name in global space: " .. instanceName)3322end33233324if (type(instance) ~= 'table' and type(instance) ~= 'function') then3325self:unregisterSuite()3326error('Name must match a function or a table: ' .. instanceName)3327end33283329table.insert(listOfNameAndInst, { name, instance })3330end33313332self:internalRunSuiteByInstances(listOfNameAndInst)3333end33343335function M.LuaUnit.run(...)3336-- Run some specific test classes.3337-- If no arguments are passed, run the class names specified on the3338-- command line. If no class name is specified on the command line3339-- run all classes whose name starts with 'Test'3340--3341-- If arguments are passed, they must be strings of the class names3342-- that you want to run or generic command line arguments (-o, -p, -v, ...)3343local runner = M.LuaUnit.new()3344return runner:runSuite(...)3345end33463347function M.LuaUnit:registerSuite()3348-- register the current instance into our global array of instances3349-- print('-> Register suite')3350M.LuaUnit.instances[#M.LuaUnit.instances + 1] = self3351end33523353function M.unregisterCurrentSuite()3354-- force unregister the last registered suite3355table.remove(M.LuaUnit.instances, #M.LuaUnit.instances)3356end33573358function M.LuaUnit:unregisterSuite()3359-- print('<- Unregister suite')3360-- remove our current instqances from the global array of instances3361local instanceIdx = nil3362for i, instance in ipairs(M.LuaUnit.instances) do3363if instance == self then3364instanceIdx = i3365break3366end3367end33683369if instanceIdx ~= nil then3370table.remove(M.LuaUnit.instances, instanceIdx)3371-- print('Unregister done')3372end33733374end33753376function M.LuaUnit:initFromArguments(...)3377--[[Parses all arguments from either command-line or direct call and set internal3378flags of LuaUnit runner according to it.33793380Return the list of names which were possibly passed on the command-line or as arguments3381]]3382local args = { ... }3383if type(args[1]) == 'table' and args[1].__class__ == 'LuaUnit' then3384-- run was called with the syntax M.LuaUnit:runSuite()3385-- we support both M.LuaUnit.run() and M.LuaUnit:run()3386-- strip out the first argument self to make it a command-line argument list3387table.remove(args, 1)3388end33893390if #args == 0 then3391args = cmdline_argv3392end33933394local options = pcall_or_abort(M.LuaUnit.parseCmdLine, args)33953396-- We expect these option fields to be either `nil` or contain3397-- valid values, so it's safe to always copy them directly.3398self.verbosity = options.verbosity3399self.quitOnError = options.quitOnError3400self.quitOnFailure = options.quitOnFailure34013402self.exeRepeat = options.exeRepeat3403self.patternIncludeFilter = options.pattern3404self.shuffle = options.shuffle34053406options.output = options.output or os.getenv('LUAUNIT_OUTPUT')3407options.fname = options.fname or os.getenv('LUAUNIT_JUNIT_FNAME')34083409if options.output then3410if options.output:lower() == 'junit' and options.fname == nil then3411print('With junit output, a filename must be supplied with -n or --name')3412os.exit(-1)3413end3414pcall_or_abort(self.setOutputType, self, options.output, options.fname)3415end34163417return options.testNames3418end34193420function M.LuaUnit:runSuite(...)3421testNames = self:initFromArguments(...)3422self:registerSuite()3423self:internalRunSuiteByNames(testNames or M.LuaUnit.collectTests())3424self:unregisterSuite()3425return self.result.notSuccessCount3426end34273428function M.LuaUnit:runSuiteByInstances(listOfNameAndInst, commandLineArguments)3429--[[3430Run all test functions or tables provided as input.34313432Input: a list of { name, instance }3433instance can either be a function or a table containing test functions starting with the prefix "test"34343435return the number of failures and errors, 0 meaning success3436]]3437-- parse the command-line arguments3438testNames = self:initFromArguments(commandLineArguments)3439self:registerSuite()3440self:internalRunSuiteByInstances(listOfNameAndInst)3441self:unregisterSuite()3442return self.result.notSuccessCount3443end3444344534463447-- class LuaUnit34483449-- For compatbility with LuaUnit v23450M.run = M.LuaUnit.run3451M.Run = M.LuaUnit.run34523453function M:setVerbosity(verbosity)3454-- set the verbosity value (as integer)3455M.LuaUnit.verbosity = verbosity3456end3457M.set_verbosity = M.setVerbosity3458M.SetVerbosity = M.setVerbosity34593460return M3461346234633464