Path: blob/main/src/resources/extensions/quarto/video/_tests/luaunit.lua
12923 views
---@diagnostic disable: unbalanced-assignments, need-check-nil, param-type-mismatch, cast-local-type, redundant-parameter1--[[2luaunit.lua34Description: A unit testing framework5Homepage: https://github.com/bluebird75/luaunit6Development by Philippe Fremy <[email protected]>7Based on initial work of Ryu, Gwang (http://www.gpgstudy.com/gpgiki/LuaUnit)8License: BSD License, see LICENSE.txt9]]--1011require("math")12local M = {}1314-- private exported functions (for testing)15M.private = {}1617M.VERSION = '3.4'18M._VERSION = M.VERSION -- For LuaUnit v2 compatibility1920-- a version which distinguish between regular Lua and LuaJit21M._LUAVERSION = (jit and jit.version) or _VERSION2223--[[ Some people like assertEquals( actual, expected ) and some people prefer24assertEquals( expected, actual ).25]]--26M.ORDER_ACTUAL_EXPECTED = true27M.PRINT_TABLE_REF_IN_ERROR_MSG = false28M.LINE_LENGTH = 8029M.TABLE_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items30M.LIST_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items3132-- this setting allow to remove entries from the stack-trace, for33-- example to hide a call to a framework which would be calling luaunit34M.STRIP_EXTRA_ENTRIES_IN_STACK_TRACE = 03536--[[ EPS is meant to help with Lua's floating point math in simple corner37cases like almostEquals(1.1-0.1, 1), which may not work as-is (e.g. on numbers38with rational binary representation) if the user doesn't provide some explicit39error margin.4041The default margin used by almostEquals() in such cases is EPS; and since42Lua may be compiled with different numeric precisions (single vs. double), we43try to select a useful default for it dynamically. Note: If the initial value44is not acceptable, it can be changed by the user to better suit specific needs.4546See also: https://en.wikipedia.org/wiki/Machine_epsilon47]]48M.EPS = 2 ^ -52 -- = machine epsilon for "double", ~2.22E-1649if math.abs(1.1 - 1 - 0.1) > M.EPS then50-- rounding error is above EPS, assume single precision51M.EPS = 2 ^ -23 -- = machine epsilon for "float", ~1.19E-0752end5354-- set this to false to debug luaunit55local STRIP_LUAUNIT_FROM_STACKTRACE = true5657M.VERBOSITY_DEFAULT = 1058M.VERBOSITY_LOW = 159M.VERBOSITY_QUIET = 060M.VERBOSITY_VERBOSE = 2061M.DEFAULT_DEEP_ANALYSIS = nil62M.FORCE_DEEP_ANALYSIS = true63M.DISABLE_DEEP_ANALYSIS = false6465-- set EXPORT_ASSERT_TO_GLOBALS to have all asserts visible as global values66-- EXPORT_ASSERT_TO_GLOBALS = true6768-- we need to keep a copy of the script args before it is overriden69local cmdline_argv = rawget(_G, "arg")7071M.FAILURE_PREFIX = 'LuaUnit test FAILURE: ' -- prefix string for failed tests72M.SUCCESS_PREFIX = 'LuaUnit test SUCCESS: ' -- prefix string for successful tests finished early73M.SKIP_PREFIX = 'LuaUnit test SKIP: ' -- prefix string for skipped tests74757677M.USAGE = [[Usage: lua <your_test_suite.lua> [options] [testname1 [testname2] ... ]78Options:79-h, --help: Print this help80--version: Print version information81-v, --verbose: Increase verbosity82-q, --quiet: Set verbosity to minimum83-e, --error: Stop on first error84-f, --failure: Stop on first failure or error85-s, --shuffle: Shuffle tests before running them86-o, --output OUTPUT: Set output type to OUTPUT87Possible values: text, tap, junit, nil88-n, --name NAME: For junit only, mandatory name of xml file89-r, --repeat NUM: Execute all tests NUM times, e.g. to trig the JIT90-p, --pattern PATTERN: Execute all test names matching the Lua PATTERN91May be repeated to include several patterns92Make sure you escape magic chars like +? with %93-x, --exclude PATTERN: Exclude all test names matching the Lua PATTERN94May be repeated to exclude several patterns95Make sure you escape magic chars like +? with %96testname1, testname2, ... : tests to run in the form of testFunction,97TestClass or TestClass.testMethod9899You may also control LuaUnit options with the following environment variables:100* LUAUNIT_OUTPUT: same as --output101* LUAUNIT_JUNIT_FNAME: same as --name ]]102103----------------------------------------------------------------104--105-- general utility functions106--107----------------------------------------------------------------108109--[[ Note on catching exit110111I have seen the case where running a big suite of test cases and one of them would112perform a os.exit(0), making the outside world think that the full test suite was executed113successfully.114115This is an attempt to mitigate this problem: we override os.exit() to now let a test116exit the framework while we are running. When we are not running, it behaves normally.117]]118119M.oldOsExit = os.exit120os.exit = function(...)121if M.LuaUnit and #M.LuaUnit.instances ~= 0 then122local msg = [[You are trying to exit but there is still a running instance of LuaUnit.123LuaUnit expects to run until the end before exiting with a complete status of successful/failed tests.124125To force exit LuaUnit while running, please call before os.exit (assuming lu is the luaunit module loaded):126127lu.unregisterCurrentSuite()128129]]130M.private.error_fmt(2, msg)131end132M.oldOsExit(...)133end134135local function pcall_or_abort(func, ...)136-- unpack is a global function for Lua 5.1, otherwise use table.unpack137local unpack = rawget(_G, "unpack") or table.unpack138local result = { pcall(func, ...) }139if not result[1] then140-- an error occurred141print(result[2]) -- error message142print()143print(M.USAGE)144os.exit(-1)145end146return unpack(result, 2)147end148149local crossTypeOrdering = {150number = 1, boolean = 2, string = 3, table = 4, other = 5151}152local crossTypeComparison = {153number = function(a, b)154return a < b155end,156string = function(a, b)157return a < b158end,159other = function(a, b)160return tostring(a) < tostring(b)161end,162}163164local function crossTypeSort(a, b)165local type_a, type_b = type(a), type(b)166if type_a == type_b then167local func = crossTypeComparison[type_a] or crossTypeComparison.other168return func(a, b)169end170type_a = crossTypeOrdering[type_a] or crossTypeOrdering.other171type_b = crossTypeOrdering[type_b] or crossTypeOrdering.other172return type_a < type_b173end174175local function __genSortedIndex(t)176-- Returns a sequence consisting of t's keys, sorted.177local sortedIndex = {}178179for key, _ in pairs(t) do180table.insert(sortedIndex, key)181end182183table.sort(sortedIndex, crossTypeSort)184return sortedIndex185end186M.private.__genSortedIndex = __genSortedIndex187188local function sortedNext(state, control)189-- Equivalent of the next() function of table iteration, but returns the190-- keys in sorted order (see __genSortedIndex and crossTypeSort).191-- The state is a temporary variable during iteration and contains the192-- sorted key table (state.sortedIdx). It also stores the last index (into193-- the keys) used by the iteration, to find the next one quickly.194local key195196--print("sortedNext: control = "..tostring(control) )197if control == nil then198-- start of iteration199state.count = #state.sortedIdx200state.lastIdx = 1201key = state.sortedIdx[1]202return key, state.t[key]203end204205-- normally, we expect the control variable to match the last key used206if control ~= state.sortedIdx[state.lastIdx] then207-- strange, we have to find the next value by ourselves208-- the key table is sorted in crossTypeSort() order! -> use bisection209local lower, upper = 1, state.count210repeat211state.lastIdx = math.modf((lower + upper) / 2)212key = state.sortedIdx[state.lastIdx]213if key == control then214break -- key found (and thus prev index)215end216if crossTypeSort(key, control) then217-- key < control, continue search "right" (towards upper bound)218lower = state.lastIdx + 1219else220-- key > control, continue search "left" (towards lower bound)221upper = state.lastIdx - 1222end223until lower > upper224if lower > upper then225-- only true if the key wasn't found, ...226state.lastIdx = state.count -- ... so ensure no match in code below227end228end229230-- proceed by retrieving the next value (or nil) from the sorted keys231state.lastIdx = state.lastIdx + 1232key = state.sortedIdx[state.lastIdx]233if key then234return key, state.t[key]235end236237-- getting here means returning `nil`, which will end the iteration238end239240local function sortedPairs(tbl)241-- Equivalent of the pairs() function on tables. Allows to iterate in242-- sorted order. As required by "generic for" loops, this will return the243-- iterator (function), an "invariant state", and the initial control value.244-- (see http://www.lua.org/pil/7.2.html)245return sortedNext, { t = tbl, sortedIdx = __genSortedIndex(tbl) }, nil246end247M.private.sortedPairs = sortedPairs248249-- seed the random with a strongly varying seed250math.randomseed(math.floor(os.clock() * 1E11))251252local function randomizeTable(t)253-- randomize the item orders of the table t254for i = #t, 2, -1 do255local j = math.random(i)256if i ~= j then257t[i], t[j] = t[j], t[i]258end259end260end261M.private.randomizeTable = randomizeTable262263local function strsplit(delimiter, text)264-- Split text into a list consisting of the strings in text, separated265-- by strings matching delimiter (which may _NOT_ be a pattern).266-- Example: strsplit(", ", "Anna, Bob, Charlie, Dolores")267if delimiter == "" or delimiter == nil then268-- this would result in endless loops269error("delimiter is nil or empty string!")270end271if text == nil then272return nil273end274275local list, pos, first, last = {}, 1276while true do277first, last = text:find(delimiter, pos, true)278if first then279-- found?280table.insert(list, text:sub(pos, first - 1))281pos = last + 1282else283table.insert(list, text:sub(pos))284break285end286end287return list288end289M.private.strsplit = strsplit290291local function hasNewLine(s)292-- return true if s has a newline293return (string.find(s, '\n', 1, true) ~= nil)294end295M.private.hasNewLine = hasNewLine296297local function prefixString(prefix, s)298-- Prefix all the lines of s with prefix299return prefix .. string.gsub(s, '\n', '\n' .. prefix)300end301M.private.prefixString = prefixString302303local function strMatch(s, pattern, start, final)304-- return true if s matches completely the pattern from index start to index end305-- return false in every other cases306-- if start is nil, matches from the beginning of the string307-- if final is nil, matches to the end of the string308start = start or 1309final = final or string.len(s)310311local foundStart, foundEnd = string.find(s, pattern, start, false)312return foundStart == start and foundEnd == final313end314M.private.strMatch = strMatch315316local function patternFilter(patterns, expr)317-- Run `expr` through the inclusion and exclusion rules defined in patterns318-- and return true if expr shall be included, false for excluded.319-- Inclusion pattern are defined as normal patterns, exclusions320-- patterns start with `!` and are followed by a normal pattern321322-- result: nil = UNKNOWN (not matched yet), true = ACCEPT, false = REJECT323-- default: true if no explicit "include" is found, set to false otherwise324local default, result = true, nil325326if patterns ~= nil then327for _, pattern in ipairs(patterns) do328local exclude = pattern:sub(1, 1) == '!'329if exclude then330pattern = pattern:sub(2)331else332-- at least one include pattern specified, a match is required333default = false334end335-- print('pattern: ',pattern)336-- print('exclude: ',exclude)337-- print('default: ',default)338339if string.find(expr, pattern) then340-- set result to false when excluding, true otherwise341result = not exclude342end343end344end345346if result ~= nil then347return result348end349return default350end351M.private.patternFilter = patternFilter352353local function xmlEscape(s)354-- Return s escaped for XML attributes355-- escapes table:356-- " "357-- ' '358-- < <359-- > >360-- & &361362return string.gsub(s, '.', {363['&'] = "&",364['"'] = """,365["'"] = "'",366['<'] = "<",367['>'] = ">",368})369end370M.private.xmlEscape = xmlEscape371372local function xmlCDataEscape(s)373-- Return s escaped for CData section, escapes: "]]>"374return string.gsub(s, ']]>', ']]>')375end376M.private.xmlCDataEscape = xmlCDataEscape377378local function lstrip(s)379--[[Return s with all leading white spaces and tabs removed]]380local idx = 0381while idx < s:len() do382idx = idx + 1383local c = s:sub(idx, idx)384if c ~= ' ' and c ~= '\t' then385break386end387end388return s:sub(idx)389end390M.private.lstrip = lstrip391392local function extractFileLineInfo(s)393--[[ From a string in the form "(leading spaces) dir1/dir2\dir3\file.lua:linenb: msg"394395Return the "file.lua:linenb" information396]]397local s2 = lstrip(s)398local firstColon = s2:find(':', 1, true)399if firstColon == nil then400-- string is not in the format file:line:401return s402end403local secondColon = s2:find(':', firstColon + 1, true)404if secondColon == nil then405-- string is not in the format file:line:406return s407end408409return s2:sub(1, secondColon - 1)410end411M.private.extractFileLineInfo = extractFileLineInfo412413local function stripLuaunitTrace2(stackTrace, errMsg)414--[[415-- Example of a traceback:416<<stack traceback:417example_with_luaunit.lua:130: in function 'test2_withFailure'418./luaunit.lua:1449: in function <./luaunit.lua:1449>419[C]: in function 'xpcall'420./luaunit.lua:1449: in function 'protectedCall'421./luaunit.lua:1508: in function 'execOneFunction'422./luaunit.lua:1596: in function 'runSuiteByInstances'423./luaunit.lua:1660: in function 'runSuiteByNames'424./luaunit.lua:1736: in function 'runSuite'425example_with_luaunit.lua:140: in main chunk426[C]: in ?>>427error message: <<example_with_luaunit.lua:130: expected 2, got 1>>428429Other example:430<<stack traceback:431./luaunit.lua:545: in function 'assertEquals'432example_with_luaunit.lua:58: in function 'TestToto.test7'433./luaunit.lua:1517: in function <./luaunit.lua:1517>434[C]: in function 'xpcall'435./luaunit.lua:1517: in function 'protectedCall'436./luaunit.lua:1578: in function 'execOneFunction'437./luaunit.lua:1677: in function 'runSuiteByInstances'438./luaunit.lua:1730: in function 'runSuiteByNames'439./luaunit.lua:1806: in function 'runSuite'440example_with_luaunit.lua:140: in main chunk441[C]: in ?>>442error message: <<example_with_luaunit.lua:58: expected 2, got 1>>443444<<stack traceback:445luaunit2/example_with_luaunit.lua:124: in function 'test1_withFailure'446luaunit2/luaunit.lua:1532: in function <luaunit2/luaunit.lua:1532>447[C]: in function 'xpcall'448luaunit2/luaunit.lua:1532: in function 'protectedCall'449luaunit2/luaunit.lua:1591: in function 'execOneFunction'450luaunit2/luaunit.lua:1679: in function 'runSuiteByInstances'451luaunit2/luaunit.lua:1743: in function 'runSuiteByNames'452luaunit2/luaunit.lua:1819: in function 'runSuite'453luaunit2/example_with_luaunit.lua:140: in main chunk454[C]: in ?>>455error message: <<luaunit2/example_with_luaunit.lua:124: expected 2, got 1>>456457458-- first line is "stack traceback": KEEP459-- next line may be luaunit line: REMOVE460-- next lines are call in the program under testOk: REMOVE461-- next lines are calls from luaunit to call the program under test: KEEP462463-- Strategy:464-- keep first line465-- remove lines that are part of luaunit466-- kepp lines until we hit a luaunit line467468The strategy for stripping is:469* keep first line "stack traceback:"470* part1:471* analyse all lines of the stack from bottom to top of the stack (first line to last line)472* extract the "file:line:" part of the line473* compare it with the "file:line" part of the error message474* if it does not match strip the line475* if it matches, keep the line and move to part 2476* part2:477* anything NOT starting with luaunit.lua is the interesting part of the stack trace478* anything starting again with luaunit.lua is part of the test launcher and should be stripped out479]]480481local function isLuaunitInternalLine(s)482-- return true if line of stack trace comes from inside luaunit483return s:find('[/\\]luaunit%.lua:%d+: ') ~= nil484end485486-- print( '<<'..stackTrace..'>>' )487488local t = strsplit('\n', stackTrace)489-- print( prettystr(t) )490491local idx = 2492493local errMsgFileLine = extractFileLineInfo(errMsg)494-- print('emfi="'..errMsgFileLine..'"')495496-- remove lines that are still part of luaunit497while t[idx] and extractFileLineInfo(t[idx]) ~= errMsgFileLine do498-- print('Removing : '..t[idx] )499table.remove(t, idx)500end501502-- keep lines until we hit luaunit again503while t[idx] and (not isLuaunitInternalLine(t[idx])) do504-- print('Keeping : '..t[idx] )505idx = idx + 1506end507508-- remove remaining luaunit lines509while t[idx] do510-- print('Removing2 : '..t[idx] )511table.remove(t, idx)512end513514-- print( prettystr(t) )515return table.concat(t, '\n')516517end518M.private.stripLuaunitTrace2 = stripLuaunitTrace2519520local function prettystr_sub(v, indentLevel, printTableRefs, cycleDetectTable)521local type_v = type(v)522if "string" == type_v then523-- use clever delimiters according to content:524-- enclose with single quotes if string contains ", but no '525if v:find('"', 1, true) and not v:find("'", 1, true) then526return "'" .. v .. "'"527end528-- use double quotes otherwise, escape embedded "529return '"' .. v:gsub('"', '\\"') .. '"'530531elseif "table" == type_v then532--if v.__class__ then533-- return string.gsub( tostring(v), 'table', v.__class__ )534--end535return M.private._table_tostring(v, indentLevel, printTableRefs, cycleDetectTable)536537elseif "number" == type_v then538-- eliminate differences in formatting between various Lua versions539if v ~= v then540return "#NaN" -- "not a number"541end542if v == math.huge then543return "#Inf" -- "infinite"544end545if v == -math.huge then546return "-#Inf"547end548if _VERSION == "Lua 5.3" then549local i = math.tointeger(v)550if i then551return tostring(i)552end553end554end555556return tostring(v)557end558559local function prettystr(v)560--[[ Pretty string conversion, to display the full content of a variable of any type.561562* string are enclosed with " by default, or with ' if string contains a "563* tables are expanded to show their full content, with indentation in case of nested tables564]]--565local cycleDetectTable = {}566local s = prettystr_sub(v, 1, M.PRINT_TABLE_REF_IN_ERROR_MSG, cycleDetectTable)567if cycleDetectTable.detected and not M.PRINT_TABLE_REF_IN_ERROR_MSG then568-- some table contain recursive references,569-- so we must recompute the value by including all table references570-- else the result looks like crap571cycleDetectTable = {}572s = prettystr_sub(v, 1, true, cycleDetectTable)573end574return s575end576M.prettystr = prettystr577578function M.adjust_err_msg_with_iter(err_msg, iter_msg)579--[[ Adjust the error message err_msg: trim the FAILURE_PREFIX or SUCCESS_PREFIX information if needed,580add the iteration message if any and return the result.581582err_msg: string, error message captured with pcall583iter_msg: a string describing the current iteration ("iteration N") or nil584if there is no iteration in this test.585586Returns: (new_err_msg, test_status)587new_err_msg: string, adjusted error message, or nil in case of success588test_status: M.NodeStatus.FAIL, SUCCESS or ERROR according to the information589contained in the error message.590]]591if iter_msg then592iter_msg = iter_msg .. ', '593else594iter_msg = ''595end596597local RE_FILE_LINE = '.*:%d+: '598599-- error message is not necessarily a string,600-- so convert the value to string with prettystr()601if type(err_msg) ~= 'string' then602err_msg = prettystr(err_msg)603end604605if (err_msg:find(M.SUCCESS_PREFIX) == 1) or err_msg:match('(' .. RE_FILE_LINE .. ')' .. M.SUCCESS_PREFIX .. ".*") then606-- test finished early with success()607return nil, M.NodeStatus.SUCCESS608end609610if (err_msg:find(M.SKIP_PREFIX) == 1) or (err_msg:match('(' .. RE_FILE_LINE .. ')' .. M.SKIP_PREFIX .. ".*") ~= nil) then611-- substitute prefix by iteration message612err_msg = err_msg:gsub('.*' .. M.SKIP_PREFIX, iter_msg, 1)613-- print("failure detected")614return err_msg, M.NodeStatus.SKIP615end616617if (err_msg:find(M.FAILURE_PREFIX) == 1) or (err_msg:match('(' .. RE_FILE_LINE .. ')' .. M.FAILURE_PREFIX .. ".*") ~= nil) then618-- substitute prefix by iteration message619err_msg = err_msg:gsub(M.FAILURE_PREFIX, iter_msg, 1)620-- print("failure detected")621return err_msg, M.NodeStatus.FAIL622end623624625626-- print("error detected")627-- regular error, not a failure628if iter_msg then629local match630-- "./test\\test_luaunit.lua:2241: some error msg631match = err_msg:match('(.*:%d+: ).*')632if match then633err_msg = err_msg:gsub(match, match .. iter_msg)634else635-- no file:line: infromation, just add the iteration info at the beginning of the line636err_msg = iter_msg .. err_msg637end638end639return err_msg, M.NodeStatus.ERROR640end641642local function tryMismatchFormatting(table_a, table_b, doDeepAnalysis, margin)643--[[644Prepares a nice error message when comparing tables, performing a deeper645analysis.646647Arguments:648* table_a, table_b: tables to be compared649* doDeepAnalysis:650M.DEFAULT_DEEP_ANALYSIS: (the default if not specified) perform deep analysis only for big lists and big dictionnaries651M.FORCE_DEEP_ANALYSIS : always perform deep analysis652M.DISABLE_DEEP_ANALYSIS: never perform deep analysis653* margin: supplied only for almost equality654655Returns: {success, result}656* success: false if deep analysis could not be performed657in this case, just use standard assertion message658* result: if success is true, a multi-line string with deep analysis of the two lists659]]660661-- check if table_a & table_b are suitable for deep analysis662if type(table_a) ~= 'table' or type(table_b) ~= 'table' then663return false664end665666if doDeepAnalysis == M.DISABLE_DEEP_ANALYSIS then667return false668end669670local len_a, len_b, isPureList = #table_a, #table_b, true671672for k1, v1 in pairs(table_a) do673if type(k1) ~= 'number' or k1 > len_a then674-- this table a mapping675isPureList = false676break677end678end679680if isPureList then681for k2, v2 in pairs(table_b) do682if type(k2) ~= 'number' or k2 > len_b then683-- this table a mapping684isPureList = false685break686end687end688end689690if isPureList and math.min(len_a, len_b) < M.LIST_DIFF_ANALYSIS_THRESHOLD then691if not (doDeepAnalysis == M.FORCE_DEEP_ANALYSIS) then692return false693end694end695696if isPureList then697return M.private.mismatchFormattingPureList(table_a, table_b, margin)698else699-- only work on mapping for the moment700-- return M.private.mismatchFormattingMapping( table_a, table_b, doDeepAnalysis )701return false702end703end704M.private.tryMismatchFormatting = tryMismatchFormatting705706local function getTaTbDescr()707if not M.ORDER_ACTUAL_EXPECTED then708return 'expected', 'actual'709end710return 'actual', 'expected'711end712713local function extendWithStrFmt(res, ...)714table.insert(res, string.format(...))715end716717local function mismatchFormattingMapping(table_a, table_b, doDeepAnalysis)718--[[719Prepares a nice error message when comparing tables which are not pure lists, performing a deeper720analysis.721722Returns: {success, result}723* success: false if deep analysis could not be performed724in this case, just use standard assertion message725* result: if success is true, a multi-line string with deep analysis of the two lists726]]727728-- disable for the moment729--[[730local result = {}731local descrTa, descrTb = getTaTbDescr()732733local keysCommon = {}734local keysOnlyTa = {}735local keysOnlyTb = {}736local keysDiffTaTb = {}737738local k, v739740for k,v in pairs( table_a ) do741if is_equal( v, table_b[k] ) then742table.insert( keysCommon, k )743else744if table_b[k] == nil then745table.insert( keysOnlyTa, k )746else747table.insert( keysDiffTaTb, k )748end749end750end751752for k,v in pairs( table_b ) do753if not is_equal( v, table_a[k] ) and table_a[k] == nil then754table.insert( keysOnlyTb, k )755end756end757758local len_a = #keysCommon + #keysDiffTaTb + #keysOnlyTa759local len_b = #keysCommon + #keysDiffTaTb + #keysOnlyTb760local limited_display = (len_a < 5 or len_b < 5)761762if math.min(len_a, len_b) < M.TABLE_DIFF_ANALYSIS_THRESHOLD then763return false764end765766if not limited_display then767if len_a == len_b then768extendWithStrFmt( result, 'Table A (%s) and B (%s) both have %d items', descrTa, descrTb, len_a )769else770extendWithStrFmt( result, 'Table A (%s) has %d items and table B (%s) has %d items', descrTa, len_a, descrTb, len_b )771end772773if #keysCommon == 0 and #keysDiffTaTb == 0 then774table.insert( result, 'Table A and B have no keys in common, they are totally different')775else776local s_other = 'other '777if #keysCommon then778extendWithStrFmt( result, 'Table A and B have %d identical items', #keysCommon )779else780table.insert( result, 'Table A and B have no identical items' )781s_other = ''782end783784if #keysDiffTaTb ~= 0 then785result[#result] = string.format( '%s and %d items differing present in both tables', result[#result], #keysDiffTaTb)786else787result[#result] = string.format( '%s and no %sitems differing present in both tables', result[#result], s_other, #keysDiffTaTb)788end789end790791extendWithStrFmt( result, 'Table A has %d keys not present in table B and table B has %d keys not present in table A', #keysOnlyTa, #keysOnlyTb )792end793794local function keytostring(k)795if "string" == type(k) and k:match("^[_%a][_%w]*$") then796return k797end798return prettystr(k)799end800801if #keysDiffTaTb ~= 0 then802table.insert( result, 'Items differing in A and B:')803for k,v in sortedPairs( keysDiffTaTb ) do804extendWithStrFmt( result, ' - A[%s]: %s', keytostring(v), prettystr(table_a[v]) )805extendWithStrFmt( result, ' + B[%s]: %s', keytostring(v), prettystr(table_b[v]) )806end807end808809if #keysOnlyTa ~= 0 then810table.insert( result, 'Items only in table A:' )811for k,v in sortedPairs( keysOnlyTa ) do812extendWithStrFmt( result, ' - A[%s]: %s', keytostring(v), prettystr(table_a[v]) )813end814end815816if #keysOnlyTb ~= 0 then817table.insert( result, 'Items only in table B:' )818for k,v in sortedPairs( keysOnlyTb ) do819extendWithStrFmt( result, ' + B[%s]: %s', keytostring(v), prettystr(table_b[v]) )820end821end822823if #keysCommon ~= 0 then824table.insert( result, 'Items common to A and B:')825for k,v in sortedPairs( keysCommon ) do826extendWithStrFmt( result, ' = A and B [%s]: %s', keytostring(v), prettystr(table_a[v]) )827end828end829830return true, table.concat( result, '\n')831]]832end833M.private.mismatchFormattingMapping = mismatchFormattingMapping834835local function mismatchFormattingPureList(table_a, table_b, margin)836--[[837Prepares a nice error message when comparing tables which are lists, performing a deeper838analysis.839840margin is supplied only for almost equality841842Returns: {success, result}843* success: false if deep analysis could not be performed844in this case, just use standard assertion message845* result: if success is true, a multi-line string with deep analysis of the two lists846]]847local result, descrTa, descrTb = {}, getTaTbDescr()848849local len_a, len_b, refa, refb = #table_a, #table_b, '', ''850if M.PRINT_TABLE_REF_IN_ERROR_MSG then851refa, refb = string.format('<%s> ', M.private.table_ref(table_a)), string.format('<%s> ', M.private.table_ref(table_b))852end853local longest, shortest = math.max(len_a, len_b), math.min(len_a, len_b)854local deltalv = longest - shortest855856local commonUntil = shortest857for i = 1, shortest do858if not M.private.is_table_equals(table_a[i], table_b[i], margin) then859commonUntil = i - 1860break861end862end863864local commonBackTo = shortest - 1865for i = 0, shortest - 1 do866if not M.private.is_table_equals(table_a[len_a - i], table_b[len_b - i], margin) then867commonBackTo = i - 1868break869end870end871872table.insert(result, 'List difference analysis:')873if len_a == len_b then874extendWithStrFmt(result, '* lists %sA (%s) and %sB (%s) have the same size', refa, descrTa, refb, descrTb)875else876extendWithStrFmt(result, '* list sizes differ: list %sA (%s) has %d items, list %sB (%s) has %d items', refa, descrTa, len_a, refb, descrTb, len_b)877end878879extendWithStrFmt(result, '* lists A and B start differing at index %d', commonUntil + 1)880if commonBackTo >= 0 then881if deltalv > 0 then882extendWithStrFmt(result, '* lists A and B are equal again from index %d for A, %d for B', len_a - commonBackTo, len_b - commonBackTo)883else884extendWithStrFmt(result, '* lists A and B are equal again from index %d', len_a - commonBackTo)885end886end887888local function insertABValue(ai, bi)889bi = bi or ai890if M.private.is_table_equals(table_a[ai], table_b[bi], margin) then891return extendWithStrFmt(result, ' = A[%d], B[%d]: %s', ai, bi, prettystr(table_a[ai]))892else893extendWithStrFmt(result, ' - A[%d]: %s', ai, prettystr(table_a[ai]))894extendWithStrFmt(result, ' + B[%d]: %s', bi, prettystr(table_b[bi]))895end896end897898-- common parts to list A & B, at the beginning899if commonUntil > 0 then900table.insert(result, '* Common parts:')901for i = 1, commonUntil do902insertABValue(i)903end904end905906-- diffing parts to list A & B907if commonUntil < shortest - commonBackTo - 1 then908table.insert(result, '* Differing parts:')909for i = commonUntil + 1, shortest - commonBackTo - 1 do910insertABValue(i)911end912end913914-- display indexes of one list, with no match on other list915if shortest - commonBackTo <= longest - commonBackTo - 1 then916table.insert(result, '* Present only in one list:')917for i = shortest - commonBackTo, longest - commonBackTo - 1 do918if len_a > len_b then919extendWithStrFmt(result, ' - A[%d]: %s', i, prettystr(table_a[i]))920-- table.insert( result, '+ (no matching B index)')921else922-- table.insert( result, '- no matching A index')923extendWithStrFmt(result, ' + B[%d]: %s', i, prettystr(table_b[i]))924end925end926end927928-- common parts to list A & B, at the end929if commonBackTo >= 0 then930table.insert(result, '* Common parts at the end of the lists')931for i = longest - commonBackTo, longest do932if len_a > len_b then933insertABValue(i, i - deltalv)934else935insertABValue(i - deltalv, i)936end937end938end939940return true, table.concat(result, '\n')941end942M.private.mismatchFormattingPureList = mismatchFormattingPureList943944local function prettystrPairs(value1, value2, suffix_a, suffix_b)945--[[946This function helps with the recurring task of constructing the "expected947vs. actual" error messages. It takes two arbitrary values and formats948corresponding strings with prettystr().949950To keep the (possibly complex) output more readable in case the resulting951strings contain line breaks, they get automatically prefixed with additional952newlines. Both suffixes are optional (default to empty strings), and get953appended to the "value1" string. "suffix_a" is used if line breaks were954encountered, "suffix_b" otherwise.955956Returns the two formatted strings (including padding/newlines).957]]958local str1, str2 = prettystr(value1), prettystr(value2)959if hasNewLine(str1) or hasNewLine(str2) then960-- line break(s) detected, add padding961return "\n" .. str1 .. (suffix_a or ""), "\n" .. str2962end963return str1 .. (suffix_b or ""), str2964end965M.private.prettystrPairs = prettystrPairs966967local UNKNOWN_REF = 'table 00-unknown ref'968local ref_generator = { value = 1, [UNKNOWN_REF] = 0 }969970local function table_ref(t)971-- return the default tostring() for tables, with the table ID, even if the table has a metatable972-- with the __tostring converter973local ref = ''974local mt = getmetatable(t)975if mt == nil then976ref = tostring(t)977else978local success, result979success, result = pcall(setmetatable, t, nil)980if not success then981-- protected table, if __tostring is defined, we can982-- not get the reference. And we can not know in advance.983ref = tostring(t)984if not ref:match('table: 0?x?[%x]+') then985return UNKNOWN_REF986end987else988ref = tostring(t)989setmetatable(t, mt)990end991end992-- strip the "table: " part993ref = ref:sub(8)994if ref ~= UNKNOWN_REF and ref_generator[ref] == nil then995-- Create a new reference number996ref_generator[ref] = ref_generator.value997ref_generator.value = ref_generator.value + 1998end999if M.PRINT_TABLE_REF_IN_ERROR_MSG then1000return string.format('table %02d-%s', ref_generator[ref], ref)1001else1002return string.format('table %02d', ref_generator[ref])1003end1004end1005M.private.table_ref = table_ref10061007local TABLE_TOSTRING_SEP = ", "1008local TABLE_TOSTRING_SEP_LEN = string.len(TABLE_TOSTRING_SEP)10091010local function _table_tostring(tbl, indentLevel, printTableRefs, cycleDetectTable)1011printTableRefs = printTableRefs or M.PRINT_TABLE_REF_IN_ERROR_MSG1012cycleDetectTable = cycleDetectTable or {}1013cycleDetectTable[tbl] = true10141015local result, dispOnMultLines = {}, false10161017-- like prettystr but do not enclose with "" if the string is just alphanumerical1018-- this is better for displaying table keys who are often simple strings1019local function keytostring(k)1020if "string" == type(k) and k:match("^[_%a][_%w]*$") then1021return k1022end1023return prettystr_sub(k, indentLevel + 1, printTableRefs, cycleDetectTable)1024end10251026local mt = getmetatable(tbl)10271028if mt and mt.__tostring then1029-- if table has a __tostring() function in its metatable, use it to display the table1030-- else, compute a regular table1031result = tostring(tbl)1032if type(result) ~= 'string' then1033return string.format('<invalid tostring() result: "%s" >', prettystr(result))1034end1035result = strsplit('\n', result)1036return M.private._table_tostring_format_multiline_string(result, indentLevel)10371038else1039-- no metatable, compute the table representation10401041local entry, count, seq_index = nil, 0, 11042for k, v in sortedPairs(tbl) do10431044-- key part1045if k == seq_index then1046-- for the sequential part of tables, we'll skip the "<key>=" output1047entry = ''1048seq_index = seq_index + 11049elseif cycleDetectTable[k] then1050-- recursion in the key detected1051cycleDetectTable.detected = true1052entry = "<" .. table_ref(k) .. ">="1053else1054entry = keytostring(k) .. "="1055end10561057-- value part1058if cycleDetectTable[v] then1059-- recursion in the value detected!1060cycleDetectTable.detected = true1061entry = entry .. "<" .. table_ref(v) .. ">"1062else1063entry = entry ..1064prettystr_sub(v, indentLevel + 1, printTableRefs, cycleDetectTable)1065end1066count = count + 11067result[count] = entry1068end1069return M.private._table_tostring_format_result(tbl, result, indentLevel, printTableRefs)1070end10711072end1073M.private._table_tostring = _table_tostring -- prettystr_sub() needs it10741075local function _table_tostring_format_multiline_string(tbl_str, indentLevel)1076local indentString = '\n' .. string.rep(" ", indentLevel - 1)1077return table.concat(tbl_str, indentString)10781079end1080M.private._table_tostring_format_multiline_string = _table_tostring_format_multiline_string10811082local function _table_tostring_format_result(tbl, result, indentLevel, printTableRefs)1083-- final function called in _table_to_string() to format the resulting list of1084-- string describing the table.10851086local dispOnMultLines = false10871088-- set dispOnMultLines to true if the maximum LINE_LENGTH would be exceeded with the values1089local totalLength = 01090for k, v in ipairs(result) do1091totalLength = totalLength + string.len(v)1092if totalLength >= M.LINE_LENGTH then1093dispOnMultLines = true1094break1095end1096end10971098-- set dispOnMultLines to true if the max LINE_LENGTH would be exceeded1099-- with the values and the separators.1100if not dispOnMultLines then1101-- adjust with length of separator(s):1102-- two items need 1 sep, three items two seps, ... plus len of '{}'1103if #result > 0 then1104totalLength = totalLength + TABLE_TOSTRING_SEP_LEN * (#result - 1)1105end1106dispOnMultLines = (totalLength + 2 >= M.LINE_LENGTH)1107end11081109-- now reformat the result table (currently holding element strings)1110if dispOnMultLines then1111local indentString = string.rep(" ", indentLevel - 1)1112result = {1113"{\n ",1114indentString,1115table.concat(result, ",\n " .. indentString),1116"\n",1117indentString,1118"}"1119}1120else1121result = { "{", table.concat(result, TABLE_TOSTRING_SEP), "}" }1122end1123if printTableRefs then1124table.insert(result, 1, "<" .. table_ref(tbl) .. "> ") -- prepend table ref1125end1126return table.concat(result)1127end1128M.private._table_tostring_format_result = _table_tostring_format_result -- prettystr_sub() needs it11291130local function table_findkeyof(t, element)1131-- Return the key k of the given element in table t, so that t[k] == element1132-- (or `nil` if element is not present within t). Note that we use our1133-- 'general' is_equal comparison for matching, so this function should1134-- handle table-type elements gracefully and consistently.1135if type(t) == "table" then1136for k, v in pairs(t) do1137if M.private.is_table_equals(v, element) then1138return k1139end1140end1141end1142return nil1143end11441145local function _is_table_items_equals(actual, expected)1146local type_a, type_e = type(actual), type(expected)11471148if type_a ~= type_e then1149return false11501151elseif (type_a == 'table') --[[and (type_e == 'table')]] then1152for k, v in pairs(actual) do1153if table_findkeyof(expected, v) == nil then1154return false -- v not contained in expected1155end1156end1157for k, v in pairs(expected) do1158if table_findkeyof(actual, v) == nil then1159return false -- v not contained in actual1160end1161end1162return true11631164elseif actual ~= expected then1165return false1166end11671168return true1169end11701171--[[1172This is a specialized metatable to help with the bookkeeping of recursions1173in _is_table_equals(). It provides an __index table that implements utility1174functions for easier management of the table. The "cached" method queries1175the state of a specific (actual,expected) pair; and the "store" method sets1176this state to the given value. The state of pairs not "seen" / visited is1177assumed to be `nil`.1178]]1179local _recursion_cache_MT = {1180__index = {1181-- Return the cached value for an (actual,expected) pair (or `nil`)1182cached = function(t, actual, expected)1183local subtable = t[actual] or {}1184return subtable[expected]1185end,11861187-- Store cached value for a specific (actual,expected) pair.1188-- Returns the value, so it's easy to use for a "tailcall" (return ...).1189store = function(t, actual, expected, value, asymmetric)1190local subtable = t[actual]1191if not subtable then1192subtable = {}1193t[actual] = subtable1194end1195subtable[expected] = value11961197-- Unless explicitly marked "asymmetric": Consider the recursion1198-- on (expected,actual) to be equivalent to (actual,expected) by1199-- default, and thus cache the value for both.1200if not asymmetric then1201t:store(expected, actual, value, true)1202end12031204return value1205end1206}1207}12081209local function _is_table_equals(actual, expected, cycleDetectTable, marginForAlmostEqual)1210--[[Returns true if both table are equal.12111212If argument marginForAlmostEqual is suppied, number comparison is done using alomstEqual instead1213of strict equality.12141215cycleDetectTable is an internal argument used during recursion on tables.1216]]1217--print('_is_table_equals( \n '..prettystr(actual)..'\n , '..prettystr(expected)..1218-- '\n , '..prettystr(cycleDetectTable)..'\n , '..prettystr(marginForAlmostEqual)..' )')12191220local type_a, type_e = type(actual), type(expected)12211222if type_a ~= type_e then1223return false -- different types won't match1224end12251226if type_a == 'number' then1227if marginForAlmostEqual ~= nil then1228return M.almostEquals(actual, expected, marginForAlmostEqual)1229else1230return actual == expected1231end1232elseif type_a ~= 'table' then1233-- other types compare directly1234return actual == expected1235end12361237cycleDetectTable = cycleDetectTable or { actual = {}, expected = {} }1238if cycleDetectTable.actual[actual] then1239-- oh, we hit a cycle in actual1240if cycleDetectTable.expected[expected] then1241-- uh, we hit a cycle at the same time in expected1242-- so the two tables have similar structure1243return true1244end12451246-- cycle was hit only in actual, the structure differs from expected1247return false1248end12491250if cycleDetectTable.expected[expected] then1251-- no cycle in actual, but cycle in expected1252-- the structure differ1253return false1254end12551256-- at this point, no table cycle detected, we are1257-- seeing this table for the first time12581259-- mark the cycle detection1260cycleDetectTable.actual[actual] = true1261cycleDetectTable.expected[expected] = true12621263local actualKeysMatched = {}1264for k, v in pairs(actual) do1265actualKeysMatched[k] = true -- Keep track of matched keys1266if not _is_table_equals(v, expected[k], cycleDetectTable, marginForAlmostEqual) then1267-- table differs on this key1268-- clear the cycle detection before returning1269cycleDetectTable.actual[actual] = nil1270cycleDetectTable.expected[expected] = nil1271return false1272end1273end12741275for k, v in pairs(expected) do1276if not actualKeysMatched[k] then1277-- Found a key that we did not see in "actual" -> mismatch1278-- clear the cycle detection before returning1279cycleDetectTable.actual[actual] = nil1280cycleDetectTable.expected[expected] = nil1281return false1282end1283-- Otherwise actual[k] was already matched against v = expected[k].1284end12851286-- all key match, we have a match !1287cycleDetectTable.actual[actual] = nil1288cycleDetectTable.expected[expected] = nil1289return true1290end1291M.private._is_table_equals = _is_table_equals12921293local function failure(main_msg, extra_msg_or_nil, level)1294-- raise an error indicating a test failure1295-- for error() compatibility we adjust "level" here (by +1), to report the1296-- calling context1297local msg1298if type(extra_msg_or_nil) == 'string' and extra_msg_or_nil:len() > 0 then1299msg = extra_msg_or_nil .. '\n' .. main_msg1300else1301msg = main_msg1302end1303error(M.FAILURE_PREFIX .. msg, (level or 1) + 1 + M.STRIP_EXTRA_ENTRIES_IN_STACK_TRACE)1304end13051306local function is_table_equals(actual, expected, marginForAlmostEqual)1307return _is_table_equals(actual, expected, nil, marginForAlmostEqual)1308end1309M.private.is_table_equals = is_table_equals13101311local function fail_fmt(level, extra_msg_or_nil, ...)1312-- failure with printf-style formatted message and given error level1313failure(string.format(...), extra_msg_or_nil, (level or 1) + 1)1314end1315M.private.fail_fmt = fail_fmt13161317local function error_fmt(level, ...)1318-- printf-style error()1319error(string.format(...), (level or 1) + 1 + M.STRIP_EXTRA_ENTRIES_IN_STACK_TRACE)1320end1321M.private.error_fmt = error_fmt13221323----------------------------------------------------------------1324--1325-- assertions1326--1327----------------------------------------------------------------13281329local function errorMsgEquality(actual, expected, doDeepAnalysis, margin)1330-- margin is supplied only for almost equal verification13311332if not M.ORDER_ACTUAL_EXPECTED then1333expected, actual = actual, expected1334end1335if type(expected) == 'string' or type(expected) == 'table' then1336local strExpected, strActual = prettystrPairs(expected, actual)1337local result = string.format("expected: %s\nactual: %s", strExpected, strActual)1338if margin then1339result = result .. '\nwere not equal by the margin of: ' .. prettystr(margin)1340end13411342-- extend with mismatch analysis if possible:1343local success, mismatchResult1344success, mismatchResult = tryMismatchFormatting(actual, expected, doDeepAnalysis, margin)1345if success then1346result = table.concat({ result, mismatchResult }, '\n')1347end1348return result1349end1350return string.format("expected: %s, actual: %s",1351prettystr(expected), prettystr(actual))1352end13531354function M.assertError(f, ...)1355-- assert that calling f with the arguments will raise an error1356-- example: assertError( f, 1, 2 ) => f(1,2) should generate an error1357if pcall(f, ...) then1358failure("Expected an error when calling function but no error generated", nil, 2)1359end1360end13611362function M.fail(msg)1363-- stops a test due to a failure1364failure(msg, nil, 2)1365end13661367function M.failIf(cond, msg)1368-- Fails a test with "msg" if condition is true1369if cond then1370failure(msg, nil, 2)1371end1372end13731374function M.skip(msg)1375-- skip a running test1376error_fmt(2, M.SKIP_PREFIX .. msg)1377end13781379function M.skipIf(cond, msg)1380-- skip a running test if condition is met1381if cond then1382error_fmt(2, M.SKIP_PREFIX .. msg)1383end1384end13851386function M.runOnlyIf(cond, msg)1387-- continue a running test if condition is met, else skip it1388if not cond then1389error_fmt(2, M.SKIP_PREFIX .. prettystr(msg))1390end1391end13921393function M.success()1394-- stops a test with a success1395error_fmt(2, M.SUCCESS_PREFIX)1396end13971398function M.successIf(cond)1399-- stops a test with a success if condition is met1400if cond then1401error_fmt(2, M.SUCCESS_PREFIX)1402end1403end140414051406------------------------------------------------------------------1407-- Equality assertions1408------------------------------------------------------------------14091410function M.assertEquals(actual, expected, extra_msg_or_nil, doDeepAnalysis)1411if type(actual) == 'table' and type(expected) == 'table' then1412if not is_table_equals(actual, expected) then1413failure(errorMsgEquality(actual, expected, doDeepAnalysis), extra_msg_or_nil, 2)1414end1415elseif type(actual) ~= type(expected) then1416failure(errorMsgEquality(actual, expected), extra_msg_or_nil, 2)1417elseif actual ~= expected then1418failure(errorMsgEquality(actual, expected), extra_msg_or_nil, 2)1419end1420end14211422function M.almostEquals(actual, expected, margin)1423if type(actual) ~= 'number' or type(expected) ~= 'number' or type(margin) ~= 'number' then1424error_fmt(3, 'almostEquals: must supply only number arguments.\nArguments supplied: %s, %s, %s',1425prettystr(actual), prettystr(expected), prettystr(margin))1426end1427if margin < 0 then1428error_fmt(3, 'almostEquals: margin must not be negative, current value is ' .. margin)1429end1430return math.abs(expected - actual) <= margin1431end14321433function M.assertAlmostEquals(actual, expected, margin, extra_msg_or_nil)1434-- check that two floats are close by margin1435margin = margin or M.EPS1436if type(margin) ~= 'number' then1437error_fmt(2, 'almostEquals: margin must be a number, not %s', prettystr(margin))1438end14391440if type(actual) == 'table' and type(expected) == 'table' then1441-- handle almost equals for table1442if not is_table_equals(actual, expected, margin) then1443failure(errorMsgEquality(actual, expected, nil, margin), extra_msg_or_nil, 2)1444end1445elseif type(actual) == 'number' and type(expected) == 'number' and type(margin) == 'number' then1446if not M.almostEquals(actual, expected, margin) then1447if not M.ORDER_ACTUAL_EXPECTED then1448expected, actual = actual, expected1449end1450local delta = math.abs(actual - expected)1451fail_fmt(2, extra_msg_or_nil, 'Values are not almost equal\n' ..1452'Actual: %s, expected: %s, delta %s above margin of %s',1453actual, expected, delta, margin)1454end1455else1456error_fmt(3, 'almostEquals: must supply only number or table arguments.\nArguments supplied: %s, %s, %s',1457prettystr(actual), prettystr(expected), prettystr(margin))1458end1459end14601461function M.assertNotEquals(actual, expected, extra_msg_or_nil)1462if type(actual) ~= type(expected) then1463return1464end14651466if type(actual) == 'table' and type(expected) == 'table' then1467if not is_table_equals(actual, expected) then1468return1469end1470elseif actual ~= expected then1471return1472end1473fail_fmt(2, extra_msg_or_nil, 'Received the not expected value: %s', prettystr(actual))1474end14751476function M.assertNotAlmostEquals(actual, expected, margin, extra_msg_or_nil)1477-- check that two floats are not close by margin1478margin = margin or M.EPS1479if M.almostEquals(actual, expected, margin) then1480if not M.ORDER_ACTUAL_EXPECTED then1481expected, actual = actual, expected1482end1483local delta = math.abs(actual - expected)1484fail_fmt(2, extra_msg_or_nil, 'Values are almost equal\nActual: %s, expected: %s' ..1485', delta %s below margin of %s',1486actual, expected, delta, margin)1487end1488end14891490function M.assertItemsEquals(actual, expected, extra_msg_or_nil)1491-- checks that the items of table expected1492-- are contained in table actual. Warning, this function1493-- is at least O(n^2)1494if not _is_table_items_equals(actual, expected) then1495expected, actual = prettystrPairs(expected, actual)1496fail_fmt(2, extra_msg_or_nil, 'Content of the tables are not identical:\nExpected: %s\nActual: %s',1497expected, actual)1498end1499end15001501------------------------------------------------------------------1502-- String assertion1503------------------------------------------------------------------15041505function M.assertStrContains(str, sub, isPattern, extra_msg_or_nil)1506-- this relies on lua string.find function1507-- a string always contains the empty string1508-- assert( type(str) == 'string', 'Argument 1 of assertStrContains() should be a string.' ) )1509-- assert( type(sub) == 'string', 'Argument 2 of assertStrContains() should be a string.' ) )1510if not string.find(str, sub, 1, not isPattern) then1511sub, str = prettystrPairs(sub, str, '\n')1512fail_fmt(2, extra_msg_or_nil, 'Could not find %s %s in string %s',1513isPattern and 'pattern' or 'substring', sub, str)1514end1515end15161517function M.assertStrIContains(str, sub, extra_msg_or_nil)1518-- this relies on lua string.find function1519-- a string always contains the empty string1520if not string.find(str:lower(), sub:lower(), 1, true) then1521sub, str = prettystrPairs(sub, str, '\n')1522fail_fmt(2, extra_msg_or_nil, 'Could not find (case insensitively) substring %s in string %s',1523sub, str)1524end1525end15261527function M.assertNotStrContains(str, sub, isPattern, extra_msg_or_nil)1528-- this relies on lua string.find function1529-- a string always contains the empty string1530if string.find(str, sub, 1, not isPattern) then1531sub, str = prettystrPairs(sub, str, '\n')1532fail_fmt(2, extra_msg_or_nil, 'Found the not expected %s %s in string %s',1533isPattern and 'pattern' or 'substring', sub, str)1534end1535end15361537function M.assertNotStrIContains(str, sub, extra_msg_or_nil)1538-- this relies on lua string.find function1539-- a string always contains the empty string1540if string.find(str:lower(), sub:lower(), 1, true) then1541sub, str = prettystrPairs(sub, str, '\n')1542fail_fmt(2, extra_msg_or_nil, 'Found (case insensitively) the not expected substring %s in string %s',1543sub, str)1544end1545end15461547function M.assertStrMatches(str, pattern, start, final, extra_msg_or_nil)1548-- Verify a full match for the string1549if not strMatch(str, pattern, start, final) then1550pattern, str = prettystrPairs(pattern, str, '\n')1551fail_fmt(2, extra_msg_or_nil, 'Could not match pattern %s with string %s',1552pattern, str)1553end1554end15551556local function _assertErrorMsgEquals(stripFileAndLine, expectedMsg, func, ...)1557local no_error, error_msg = pcall(func, ...)1558if no_error then1559failure('No error generated when calling function but expected error: ' .. M.prettystr(expectedMsg), nil, 3)1560end1561if type(expectedMsg) == "string" and type(error_msg) ~= "string" then1562-- table are converted to string automatically1563error_msg = tostring(error_msg)1564end1565local differ = false1566if stripFileAndLine then1567if error_msg:gsub("^.+:%d+: ", "") ~= expectedMsg then1568differ = true1569end1570else1571if error_msg ~= expectedMsg then1572local tr = type(error_msg)1573local te = type(expectedMsg)1574if te == 'table' then1575if tr ~= 'table' then1576differ = true1577else1578local ok = pcall(M.assertItemsEquals, error_msg, expectedMsg)1579if not ok then1580differ = true1581end1582end1583else1584differ = true1585end1586end1587end15881589if differ then1590error_msg, expectedMsg = prettystrPairs(error_msg, expectedMsg)1591fail_fmt(3, nil, 'Error message expected: %s\nError message received: %s\n',1592expectedMsg, error_msg)1593end1594end15951596function M.assertErrorMsgEquals(expectedMsg, func, ...)1597-- assert that calling f with the arguments will raise an error1598-- example: assertError( f, 1, 2 ) => f(1,2) should generate an error1599_assertErrorMsgEquals(false, expectedMsg, func, ...)1600end16011602function M.assertErrorMsgContentEquals(expectedMsg, func, ...)1603_assertErrorMsgEquals(true, expectedMsg, func, ...)1604end16051606function M.assertErrorMsgContains(partialMsg, func, ...)1607-- assert that calling f with the arguments will raise an error1608-- example: assertError( f, 1, 2 ) => f(1,2) should generate an error1609local no_error, error_msg = pcall(func, ...)1610if no_error then1611failure('No error generated when calling function but expected error containing: ' .. prettystr(partialMsg), nil, 2)1612end1613if type(error_msg) ~= "string" then1614error_msg = tostring(error_msg)1615end1616if not string.find(error_msg, partialMsg, nil, true) then1617error_msg, partialMsg = prettystrPairs(error_msg, partialMsg)1618fail_fmt(2, nil, 'Error message does not contain: %s\nError message received: %s\n',1619partialMsg, error_msg)1620end1621end16221623function M.assertErrorMsgMatches(expectedMsg, func, ...)1624-- assert that calling f with the arguments will raise an error1625-- example: assertError( f, 1, 2 ) => f(1,2) should generate an error1626local no_error, error_msg = pcall(func, ...)1627if no_error then1628failure('No error generated when calling function but expected error matching: "' .. expectedMsg .. '"', nil, 2)1629end1630if type(error_msg) ~= "string" then1631error_msg = tostring(error_msg)1632end1633if not strMatch(error_msg, expectedMsg) then1634expectedMsg, error_msg = prettystrPairs(expectedMsg, error_msg)1635fail_fmt(2, nil, 'Error message does not match pattern: %s\nError message received: %s\n',1636expectedMsg, error_msg)1637end1638end16391640------------------------------------------------------------------1641-- Type assertions1642------------------------------------------------------------------16431644function M.assertEvalToTrue(value, extra_msg_or_nil)1645if not value then1646failure("expected: a value evaluating to true, actual: " .. prettystr(value), extra_msg_or_nil, 2)1647end1648end16491650function M.assertEvalToFalse(value, extra_msg_or_nil)1651if value then1652failure("expected: false or nil, actual: " .. prettystr(value), extra_msg_or_nil, 2)1653end1654end16551656function M.assertIsTrue(value, extra_msg_or_nil)1657if value ~= true then1658failure("expected: true, actual: " .. prettystr(value), extra_msg_or_nil, 2)1659end1660end16611662function M.assertNotIsTrue(value, extra_msg_or_nil)1663if value == true then1664failure("expected: not true, actual: " .. prettystr(value), extra_msg_or_nil, 2)1665end1666end16671668function M.assertIsFalse(value, extra_msg_or_nil)1669if value ~= false then1670failure("expected: false, actual: " .. prettystr(value), extra_msg_or_nil, 2)1671end1672end16731674function M.assertNotIsFalse(value, extra_msg_or_nil)1675if value == false then1676failure("expected: not false, actual: " .. prettystr(value), extra_msg_or_nil, 2)1677end1678end16791680function M.assertIsNil(value, extra_msg_or_nil)1681if value ~= nil then1682failure("expected: nil, actual: " .. prettystr(value), extra_msg_or_nil, 2)1683end1684end16851686function M.assertNotIsNil(value, extra_msg_or_nil)1687if value == nil then1688failure("expected: not nil, actual: nil", extra_msg_or_nil, 2)1689end1690end16911692--[[1693Add type assertion functions to the module table M. Each of these functions1694takes a single parameter "value", and checks that its Lua type matches the1695expected string (derived from the function name):16961697M.assertIsXxx(value) -> ensure that type(value) conforms to "xxx"1698]]1699for _, funcName in ipairs(1700{ 'assertIsNumber', 'assertIsString', 'assertIsTable', 'assertIsBoolean',1701'assertIsFunction', 'assertIsUserdata', 'assertIsThread' }1702) do1703local typeExpected = funcName:match("^assertIs([A-Z]%a*)$")1704-- Lua type() always returns lowercase, also make sure the match() succeeded1705typeExpected = typeExpected and typeExpected:lower()1706or error("bad function name '" .. funcName .. "' for type assertion")17071708M[funcName] = function(value, extra_msg_or_nil)1709if type(value) ~= typeExpected then1710if type(value) == 'nil' then1711fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: nil',1712typeExpected, type(value), prettystrPairs(value))1713else1714fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: type %s, value %s',1715typeExpected, type(value), prettystrPairs(value))1716end1717end1718end1719end17201721--[[1722Add shortcuts for verifying type of a variable, without failure (luaunit v2 compatibility)1723M.isXxx(value) -> returns true if type(value) conforms to "xxx"1724]]1725for _, typeExpected in ipairs(1726{ 'Number', 'String', 'Table', 'Boolean',1727'Function', 'Userdata', 'Thread', 'Nil' }1728) do1729local typeExpectedLower = typeExpected:lower()1730local isType = function(value)1731return (type(value) == typeExpectedLower)1732end1733M['is' .. typeExpected] = isType1734M['is_' .. typeExpectedLower] = isType1735end17361737--[[1738Add non-type assertion functions to the module table M. Each of these functions1739takes a single parameter "value", and checks that its Lua type differs from the1740expected string (derived from the function name):17411742M.assertNotIsXxx(value) -> ensure that type(value) is not "xxx"1743]]1744for _, funcName in ipairs(1745{ 'assertNotIsNumber', 'assertNotIsString', 'assertNotIsTable', 'assertNotIsBoolean',1746'assertNotIsFunction', 'assertNotIsUserdata', 'assertNotIsThread' }1747) do1748local typeUnexpected = funcName:match("^assertNotIs([A-Z]%a*)$")1749-- Lua type() always returns lowercase, also make sure the match() succeeded1750typeUnexpected = typeUnexpected and typeUnexpected:lower()1751or error("bad function name '" .. funcName .. "' for type assertion")17521753M[funcName] = function(value, extra_msg_or_nil)1754if type(value) == typeUnexpected then1755fail_fmt(2, extra_msg_or_nil, 'expected: not a %s type, actual: value %s',1756typeUnexpected, prettystrPairs(value))1757end1758end1759end17601761function M.assertIs(actual, expected, extra_msg_or_nil)1762if actual ~= expected then1763if not M.ORDER_ACTUAL_EXPECTED then1764actual, expected = expected, actual1765end1766local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG1767M.PRINT_TABLE_REF_IN_ERROR_MSG = true1768expected, actual = prettystrPairs(expected, actual, '\n', '')1769M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg1770fail_fmt(2, extra_msg_or_nil, 'expected and actual object should not be different\nExpected: %s\nReceived: %s',1771expected, actual)1772end1773end17741775function M.assertNotIs(actual, expected, extra_msg_or_nil)1776if actual == expected then1777local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG1778M.PRINT_TABLE_REF_IN_ERROR_MSG = true1779local s_expected1780if not M.ORDER_ACTUAL_EXPECTED then1781s_expected = prettystrPairs(actual)1782else1783s_expected = prettystrPairs(expected)1784end1785M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg1786fail_fmt(2, extra_msg_or_nil, 'expected and actual object should be different: %s', s_expected)1787end1788end178917901791------------------------------------------------------------------1792-- Scientific assertions1793------------------------------------------------------------------179417951796function M.assertIsNaN(value, extra_msg_or_nil)1797if type(value) ~= "number" or value == value then1798failure("expected: NaN, actual: " .. prettystr(value), extra_msg_or_nil, 2)1799end1800end18011802function M.assertNotIsNaN(value, extra_msg_or_nil)1803if type(value) == "number" and value ~= value then1804failure("expected: not NaN, actual: NaN", extra_msg_or_nil, 2)1805end1806end18071808function M.assertIsInf(value, extra_msg_or_nil)1809if type(value) ~= "number" or math.abs(value) ~= math.huge then1810failure("expected: #Inf, actual: " .. prettystr(value), extra_msg_or_nil, 2)1811end1812end18131814function M.assertIsPlusInf(value, extra_msg_or_nil)1815if type(value) ~= "number" or value ~= math.huge then1816failure("expected: #Inf, actual: " .. prettystr(value), extra_msg_or_nil, 2)1817end1818end18191820function M.assertIsMinusInf(value, extra_msg_or_nil)1821if type(value) ~= "number" or value ~= -math.huge then1822failure("expected: -#Inf, actual: " .. prettystr(value), extra_msg_or_nil, 2)1823end1824end18251826function M.assertNotIsPlusInf(value, extra_msg_or_nil)1827if type(value) == "number" and value == math.huge then1828failure("expected: not #Inf, actual: #Inf", extra_msg_or_nil, 2)1829end1830end18311832function M.assertNotIsMinusInf(value, extra_msg_or_nil)1833if type(value) == "number" and value == -math.huge then1834failure("expected: not -#Inf, actual: -#Inf", extra_msg_or_nil, 2)1835end1836end18371838function M.assertNotIsInf(value, extra_msg_or_nil)1839if type(value) == "number" and math.abs(value) == math.huge then1840failure("expected: not infinity, actual: " .. prettystr(value), extra_msg_or_nil, 2)1841end1842end18431844function M.assertIsPlusZero(value, extra_msg_or_nil)1845if type(value) ~= 'number' or value ~= 0 then1846failure("expected: +0.0, actual: " .. prettystr(value), extra_msg_or_nil, 2)1847else1848if (1 / value == -math.huge) then1849-- more precise error diagnosis1850failure("expected: +0.0, actual: -0.0", extra_msg_or_nil, 2)1851else1852if (1 / value ~= math.huge) then1853-- strange, case should have already been covered1854failure("expected: +0.0, actual: " .. prettystr(value), extra_msg_or_nil, 2)1855end1856end1857end1858end18591860function M.assertIsMinusZero(value, extra_msg_or_nil)1861if type(value) ~= 'number' or value ~= 0 then1862failure("expected: -0.0, actual: " .. prettystr(value), extra_msg_or_nil, 2)1863else1864if (1 / value == math.huge) then1865-- more precise error diagnosis1866failure("expected: -0.0, actual: +0.0", extra_msg_or_nil, 2)1867else1868if (1 / value ~= -math.huge) then1869-- strange, case should have already been covered1870failure("expected: -0.0, actual: " .. prettystr(value), extra_msg_or_nil, 2)1871end1872end1873end1874end18751876function M.assertNotIsPlusZero(value, extra_msg_or_nil)1877if type(value) == 'number' and (1 / value == math.huge) then1878failure("expected: not +0.0, actual: +0.0", extra_msg_or_nil, 2)1879end1880end18811882function M.assertNotIsMinusZero(value, extra_msg_or_nil)1883if type(value) == 'number' and (1 / value == -math.huge) then1884failure("expected: not -0.0, actual: -0.0", extra_msg_or_nil, 2)1885end1886end18871888function M.assertTableContains(t, expected, extra_msg_or_nil)1889-- checks that table t contains the expected element1890if table_findkeyof(t, expected) == nil then1891t, expected = prettystrPairs(t, expected)1892fail_fmt(2, extra_msg_or_nil, 'Table %s does NOT contain the expected element %s',1893t, expected)1894end1895end18961897function M.assertNotTableContains(t, expected, extra_msg_or_nil)1898-- checks that table t doesn't contain the expected element1899local k = table_findkeyof(t, expected)1900if k ~= nil then1901t, expected = prettystrPairs(t, expected)1902fail_fmt(2, extra_msg_or_nil, 'Table %s DOES contain the unwanted element %s (at key %s)',1903t, expected, prettystr(k))1904end1905end19061907----------------------------------------------------------------1908-- Compatibility layer1909----------------------------------------------------------------19101911-- for compatibility with LuaUnit v2.x1912function M.wrapFunctions()1913-- In LuaUnit version <= 2.1 , this function was necessary to include1914-- a test function inside the global test suite. Nowadays, the functions1915-- are simply run directly as part of the test discovery process.1916-- so just do nothing !1917io.stderr:write [[Use of WrapFunctions() is no longer needed.1918Just prefix your test function names with "test" or "Test" and they1919will be picked up and run by LuaUnit.1920]]1921end19221923local list_of_funcs = {1924-- { official function name , alias }19251926-- general assertions1927{ 'assertEquals', 'assert_equals' },1928{ 'assertItemsEquals', 'assert_items_equals' },1929{ 'assertNotEquals', 'assert_not_equals' },1930{ 'assertAlmostEquals', 'assert_almost_equals' },1931{ 'assertNotAlmostEquals', 'assert_not_almost_equals' },1932{ 'assertEvalToTrue', 'assert_eval_to_true' },1933{ 'assertEvalToFalse', 'assert_eval_to_false' },1934{ 'assertStrContains', 'assert_str_contains' },1935{ 'assertStrIContains', 'assert_str_icontains' },1936{ 'assertNotStrContains', 'assert_not_str_contains' },1937{ 'assertNotStrIContains', 'assert_not_str_icontains' },1938{ 'assertStrMatches', 'assert_str_matches' },1939{ 'assertError', 'assert_error' },1940{ 'assertErrorMsgEquals', 'assert_error_msg_equals' },1941{ 'assertErrorMsgContains', 'assert_error_msg_contains' },1942{ 'assertErrorMsgMatches', 'assert_error_msg_matches' },1943{ 'assertErrorMsgContentEquals', 'assert_error_msg_content_equals' },1944{ 'assertIs', 'assert_is' },1945{ 'assertNotIs', 'assert_not_is' },1946{ 'assertTableContains', 'assert_table_contains' },1947{ 'assertNotTableContains', 'assert_not_table_contains' },1948{ 'wrapFunctions', 'WrapFunctions' },1949{ 'wrapFunctions', 'wrap_functions' },19501951-- type assertions: assertIsXXX -> assert_is_xxx1952{ 'assertIsNumber', 'assert_is_number' },1953{ 'assertIsString', 'assert_is_string' },1954{ 'assertIsTable', 'assert_is_table' },1955{ 'assertIsBoolean', 'assert_is_boolean' },1956{ 'assertIsNil', 'assert_is_nil' },1957{ 'assertIsTrue', 'assert_is_true' },1958{ 'assertIsFalse', 'assert_is_false' },1959{ 'assertIsNaN', 'assert_is_nan' },1960{ 'assertIsInf', 'assert_is_inf' },1961{ 'assertIsPlusInf', 'assert_is_plus_inf' },1962{ 'assertIsMinusInf', 'assert_is_minus_inf' },1963{ 'assertIsPlusZero', 'assert_is_plus_zero' },1964{ 'assertIsMinusZero', 'assert_is_minus_zero' },1965{ 'assertIsFunction', 'assert_is_function' },1966{ 'assertIsThread', 'assert_is_thread' },1967{ 'assertIsUserdata', 'assert_is_userdata' },19681969-- type assertions: assertIsXXX -> assertXxx1970{ 'assertIsNumber', 'assertNumber' },1971{ 'assertIsString', 'assertString' },1972{ 'assertIsTable', 'assertTable' },1973{ 'assertIsBoolean', 'assertBoolean' },1974{ 'assertIsNil', 'assertNil' },1975{ 'assertIsTrue', 'assertTrue' },1976{ 'assertIsFalse', 'assertFalse' },1977{ 'assertIsNaN', 'assertNaN' },1978{ 'assertIsInf', 'assertInf' },1979{ 'assertIsPlusInf', 'assertPlusInf' },1980{ 'assertIsMinusInf', 'assertMinusInf' },1981{ 'assertIsPlusZero', 'assertPlusZero' },1982{ 'assertIsMinusZero', 'assertMinusZero' },1983{ 'assertIsFunction', 'assertFunction' },1984{ 'assertIsThread', 'assertThread' },1985{ 'assertIsUserdata', 'assertUserdata' },19861987-- type assertions: assertIsXXX -> assert_xxx (luaunit v2 compat)1988{ 'assertIsNumber', 'assert_number' },1989{ 'assertIsString', 'assert_string' },1990{ 'assertIsTable', 'assert_table' },1991{ 'assertIsBoolean', 'assert_boolean' },1992{ 'assertIsNil', 'assert_nil' },1993{ 'assertIsTrue', 'assert_true' },1994{ 'assertIsFalse', 'assert_false' },1995{ 'assertIsNaN', 'assert_nan' },1996{ 'assertIsInf', 'assert_inf' },1997{ 'assertIsPlusInf', 'assert_plus_inf' },1998{ 'assertIsMinusInf', 'assert_minus_inf' },1999{ 'assertIsPlusZero', 'assert_plus_zero' },2000{ 'assertIsMinusZero', 'assert_minus_zero' },2001{ 'assertIsFunction', 'assert_function' },2002{ 'assertIsThread', 'assert_thread' },2003{ 'assertIsUserdata', 'assert_userdata' },20042005-- type assertions: assertNotIsXXX -> assert_not_is_xxx2006{ 'assertNotIsNumber', 'assert_not_is_number' },2007{ 'assertNotIsString', 'assert_not_is_string' },2008{ 'assertNotIsTable', 'assert_not_is_table' },2009{ 'assertNotIsBoolean', 'assert_not_is_boolean' },2010{ 'assertNotIsNil', 'assert_not_is_nil' },2011{ 'assertNotIsTrue', 'assert_not_is_true' },2012{ 'assertNotIsFalse', 'assert_not_is_false' },2013{ 'assertNotIsNaN', 'assert_not_is_nan' },2014{ 'assertNotIsInf', 'assert_not_is_inf' },2015{ 'assertNotIsPlusInf', 'assert_not_plus_inf' },2016{ 'assertNotIsMinusInf', 'assert_not_minus_inf' },2017{ 'assertNotIsPlusZero', 'assert_not_plus_zero' },2018{ 'assertNotIsMinusZero', 'assert_not_minus_zero' },2019{ 'assertNotIsFunction', 'assert_not_is_function' },2020{ 'assertNotIsThread', 'assert_not_is_thread' },2021{ 'assertNotIsUserdata', 'assert_not_is_userdata' },20222023-- type assertions: assertNotIsXXX -> assertNotXxx (luaunit v2 compat)2024{ 'assertNotIsNumber', 'assertNotNumber' },2025{ 'assertNotIsString', 'assertNotString' },2026{ 'assertNotIsTable', 'assertNotTable' },2027{ 'assertNotIsBoolean', 'assertNotBoolean' },2028{ 'assertNotIsNil', 'assertNotNil' },2029{ 'assertNotIsTrue', 'assertNotTrue' },2030{ 'assertNotIsFalse', 'assertNotFalse' },2031{ 'assertNotIsNaN', 'assertNotNaN' },2032{ 'assertNotIsInf', 'assertNotInf' },2033{ 'assertNotIsPlusInf', 'assertNotPlusInf' },2034{ 'assertNotIsMinusInf', 'assertNotMinusInf' },2035{ 'assertNotIsPlusZero', 'assertNotPlusZero' },2036{ 'assertNotIsMinusZero', 'assertNotMinusZero' },2037{ 'assertNotIsFunction', 'assertNotFunction' },2038{ 'assertNotIsThread', 'assertNotThread' },2039{ 'assertNotIsUserdata', 'assertNotUserdata' },20402041-- type assertions: assertNotIsXXX -> assert_not_xxx2042{ 'assertNotIsNumber', 'assert_not_number' },2043{ 'assertNotIsString', 'assert_not_string' },2044{ 'assertNotIsTable', 'assert_not_table' },2045{ 'assertNotIsBoolean', 'assert_not_boolean' },2046{ 'assertNotIsNil', 'assert_not_nil' },2047{ 'assertNotIsTrue', 'assert_not_true' },2048{ 'assertNotIsFalse', 'assert_not_false' },2049{ 'assertNotIsNaN', 'assert_not_nan' },2050{ 'assertNotIsInf', 'assert_not_inf' },2051{ 'assertNotIsPlusInf', 'assert_not_plus_inf' },2052{ 'assertNotIsMinusInf', 'assert_not_minus_inf' },2053{ 'assertNotIsPlusZero', 'assert_not_plus_zero' },2054{ 'assertNotIsMinusZero', 'assert_not_minus_zero' },2055{ 'assertNotIsFunction', 'assert_not_function' },2056{ 'assertNotIsThread', 'assert_not_thread' },2057{ 'assertNotIsUserdata', 'assert_not_userdata' },20582059-- all assertions with Coroutine duplicate Thread assertions2060{ 'assertIsThread', 'assertIsCoroutine' },2061{ 'assertIsThread', 'assertCoroutine' },2062{ 'assertIsThread', 'assert_is_coroutine' },2063{ 'assertIsThread', 'assert_coroutine' },2064{ 'assertNotIsThread', 'assertNotIsCoroutine' },2065{ 'assertNotIsThread', 'assertNotCoroutine' },2066{ 'assertNotIsThread', 'assert_not_is_coroutine' },2067{ 'assertNotIsThread', 'assert_not_coroutine' },2068}20692070-- Create all aliases in M2071for _, v in ipairs(list_of_funcs) do2072local funcname, alias = v[1], v[2]2073M[alias] = M[funcname]20742075if EXPORT_ASSERT_TO_GLOBALS then2076_G[funcname] = M[funcname]2077_G[alias] = M[funcname]2078end2079end20802081----------------------------------------------------------------2082--2083-- Outputters2084--2085----------------------------------------------------------------20862087-- A common "base" class for outputters2088-- For concepts involved (class inheritance) see http://www.lua.org/pil/16.2.html20892090local genericOutput = { __class__ = 'genericOutput' } -- class2091local genericOutput_MT = { __index = genericOutput } -- metatable2092M.genericOutput = genericOutput -- publish, so that custom classes may derive from it20932094function genericOutput.new(runner, default_verbosity)2095-- runner is the "parent" object controlling the output, usually a LuaUnit instance2096local t = { runner = runner }2097if runner then2098t.result = runner.result2099t.verbosity = runner.verbosity or default_verbosity2100t.fname = runner.fname2101else2102t.verbosity = default_verbosity2103end2104return setmetatable(t, genericOutput_MT)2105end21062107-- abstract ("empty") methods2108function genericOutput:startSuite()2109-- Called once, when the suite is started2110end21112112function genericOutput:startClass(class_name)2113-- Called each time a new test class is started2114end21152116function genericOutput:startTest(testName)2117-- called each time a new test is started, right before the setUp()2118-- the current test status node is already created and available in: self.result.currentNode2119end21202121function genericOutput:updateStatus(node)2122-- called with status failed or error as soon as the error/failure is encountered2123-- this method is NOT called for a successful test because a test is marked as successful by default2124-- and does not need to be updated2125end21262127function genericOutput:endTest(node)2128-- called when the test is finished, after the tearDown() method2129end21302131function genericOutput:endClass()2132-- called when executing the class is finished, before moving on to the next class of at the end of the test execution2133end21342135function genericOutput:endSuite()2136-- called at the end of the test suite execution2137end213821392140----------------------------------------------------------------2141-- class TapOutput2142----------------------------------------------------------------21432144local TapOutput = genericOutput.new() -- derived class2145local TapOutput_MT = { __index = TapOutput } -- metatable2146TapOutput.__class__ = 'TapOutput'21472148-- For a good reference for TAP format, check: http://testanything.org/tap-specification.html21492150function TapOutput.new(runner)2151local t = genericOutput.new(runner, M.VERBOSITY_LOW)2152return setmetatable(t, TapOutput_MT)2153end2154function TapOutput:startSuite()2155print("1.." .. self.result.selectedCount)2156print('# Started on ' .. self.result.startDate)2157end2158function TapOutput:startClass(class_name)2159if class_name ~= '[TestFunctions]' then2160print('# Starting class: ' .. class_name)2161end2162end21632164function TapOutput:updateStatus(node)2165if node:isSkipped() then2166io.stdout:write("ok ", self.result.currentTestNumber, "\t# SKIP ", node.msg, "\n")2167return2168end21692170io.stdout:write("not ok ", self.result.currentTestNumber, "\t", node.testName, "\n")2171if self.verbosity > M.VERBOSITY_LOW then2172print(prefixString('# ', node.msg))2173end2174if (node:isFailure() or node:isError()) and self.verbosity > M.VERBOSITY_DEFAULT then2175print(prefixString('# ', node.stackTrace))2176end2177end21782179function TapOutput:endTest(node)2180if node:isSuccess() then2181io.stdout:write("ok ", self.result.currentTestNumber, "\t", node.testName, "\n")2182end2183end21842185function TapOutput:endSuite()2186print('# ' .. M.LuaUnit.statusLine(self.result))2187return self.result.notSuccessCount2188end218921902191-- class TapOutput end21922193----------------------------------------------------------------2194-- class JUnitOutput2195----------------------------------------------------------------21962197-- See directory junitxml for more information about the junit format2198local JUnitOutput = genericOutput.new() -- derived class2199local JUnitOutput_MT = { __index = JUnitOutput } -- metatable2200JUnitOutput.__class__ = 'JUnitOutput'22012202function JUnitOutput.new(runner)2203local t = genericOutput.new(runner, M.VERBOSITY_LOW)2204t.testList = {}2205return setmetatable(t, JUnitOutput_MT)2206end22072208function JUnitOutput:startSuite()2209-- open xml file early to deal with errors2210if self.fname == nil then2211error('With Junit, an output filename must be supplied with --name!')2212end2213if string.sub(self.fname, -4) ~= '.xml' then2214self.fname = self.fname .. '.xml'2215end2216self.fd = io.open(self.fname, "w")2217if self.fd == nil then2218error("Could not open file for writing: " .. self.fname)2219end22202221print('# XML output to ' .. self.fname)2222print('# Started on ' .. self.result.startDate)2223end2224function JUnitOutput:startClass(class_name)2225if class_name ~= '[TestFunctions]' then2226print('# Starting class: ' .. class_name)2227end2228end2229function JUnitOutput:startTest(testName)2230print('# Starting test: ' .. testName)2231end22322233function JUnitOutput:updateStatus(node)2234if node:isFailure() then2235print('# Failure: ' .. prefixString('# ', node.msg):sub(4, nil))2236-- print('# ' .. node.stackTrace)2237elseif node:isError() then2238print('# Error: ' .. prefixString('# ', node.msg):sub(4, nil))2239-- print('# ' .. node.stackTrace)2240end2241end22422243function JUnitOutput:endSuite()2244print('# ' .. M.LuaUnit.statusLine(self.result))22452246-- XML file writing2247self.fd:write('<?xml version="1.0" encoding="UTF-8" ?>\n')2248self.fd:write('<testsuites>\n')2249self.fd:write(string.format(2250' <testsuite name="LuaUnit" id="00001" package="" hostname="localhost" tests="%d" timestamp="%s" time="%0.3f" errors="%d" failures="%d" skipped="%d">\n',2251self.result.runCount, self.result.startIsodate, self.result.duration, self.result.errorCount, self.result.failureCount, self.result.skippedCount))2252self.fd:write(" <properties>\n")2253self.fd:write(string.format(' <property name="Lua Version" value="%s"/>\n', _VERSION))2254self.fd:write(string.format(' <property name="LuaUnit Version" value="%s"/>\n', M.VERSION))2255-- XXX please include system name and version if possible2256self.fd:write(" </properties>\n")22572258for i, node in ipairs(self.result.allTests) do2259self.fd:write(string.format(' <testcase class_name="%s" name="%s" time="%0.3f">\n',2260node.class_name, node.testName, node.duration))2261if node:isNotSuccess() then2262self.fd:write(node:statusXML())2263end2264self.fd:write(' </testcase>\n')2265end22662267-- Next two lines are needed to validate junit ANT xsd, but really not useful in general:2268self.fd:write(' <system-out/>\n')2269self.fd:write(' <system-err/>\n')22702271self.fd:write(' </testsuite>\n')2272self.fd:write('</testsuites>\n')2273self.fd:close()2274return self.result.notSuccessCount2275end227622772278-- class TapOutput end22792280----------------------------------------------------------------2281-- class TextOutput2282----------------------------------------------------------------22832284--[[ Example of other unit-tests suite text output22852286-- Python Non verbose:22872288For each test: . or F or E22892290If some failed tests:2291==============2292ERROR / FAILURE: TestName (testfile.testclass)2293---------2294Stack trace229522962297then --------------2298then "Ran x tests in 0.000s"2299then OK or FAILED (failures=1, error=1)23002301-- Python Verbose:2302testname (filename.class_name) ... ok2303testname (filename.class_name) ... FAIL2304testname (filename.class_name) ... ERROR23052306then --------------2307then "Ran x tests in 0.000s"2308then OK or FAILED (failures=1, error=1)23092310-- Ruby:2311Started2312.2313Finished in 0.002695 seconds.231423151 tests, 2 assertions, 0 failures, 0 errors23162317-- Ruby:2318>> ruby tc_simple_number2.rb2319Loaded suite tc_simple_number22320Started2321F..2322Finished in 0.038617 seconds.232323241) Failure:2325test_failure(TestSimpleNumber) [tc_simple_number2.rb:16]:2326Adding doesn't work.2327<3> expected but was2328<4>.232923303 tests, 4 assertions, 1 failures, 0 errors23312332-- Java Junit2333.......F.2334Time: 0,0032335There was 1 failure:23361) testCapacity(junit.samples.VectorTest)junit.framework.AssertionFailedError2337at junit.samples.VectorTest.testCapacity(VectorTest.java:87)2338at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)2339at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)2340at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)23412342FAILURES!!!2343Tests run: 8, Failures: 1, Errors: 0234423452346-- Maven23472348# mvn test2349-------------------------------------------------------2350T E S T S2351-------------------------------------------------------2352Running math.AdditionTest2353Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed:23540.03 sec <<< FAILURE!23552356Results :23572358Failed tests:2359testLireSymbole(math.AdditionTest)23602361Tests run: 2, Failures: 1, Errors: 0, Skipped: 0236223632364-- LuaUnit2365---- non verbose2366* display . or F or E when running tests2367---- verbose2368* display test name + ok/fail2369----2370* blank line2371* number) ERROR or FAILURE: TestName2372Stack trace2373* blank line2374* number) ERROR or FAILURE: TestName2375Stack trace23762377then --------------2378then "Ran x tests in 0.000s (%d not selected, %d skipped)"2379then OK or FAILED (failures=1, error=1)238023812382]]23832384local TextOutput = genericOutput.new() -- derived class2385local TextOutput_MT = { __index = TextOutput } -- metatable2386TextOutput.__class__ = 'TextOutput'23872388function TextOutput.new(runner)2389local t = genericOutput.new(runner, M.VERBOSITY_DEFAULT)2390t.errorList = {}2391return setmetatable(t, TextOutput_MT)2392end23932394function TextOutput:startSuite()2395if self.verbosity > M.VERBOSITY_DEFAULT then2396print('Started on ' .. self.result.startDate)2397end2398end23992400function TextOutput:startTest(testName)2401if self.verbosity > M.VERBOSITY_DEFAULT then2402io.stdout:write(" ", self.result.currentNode.testName, " ... ")2403end2404end24052406function TextOutput:endTest(node)2407if node:isSuccess() then2408if self.verbosity > M.VERBOSITY_DEFAULT then2409io.stdout:write("Ok\n")2410else2411io.stdout:write(".")2412io.stdout:flush()2413end2414else2415if self.verbosity > M.VERBOSITY_DEFAULT then2416print(node.status)2417print(node.msg)2418--[[2419-- find out when to do this:2420if self.verbosity > M.VERBOSITY_DEFAULT then2421print( node.stackTrace )2422end2423]]2424else2425-- write only the first character of status E, F or S2426io.stdout:write(string.sub(node.status, 1, 1))2427io.stdout:flush()2428end2429end2430end24312432function TextOutput:displayOneFailedTest(index, fail)2433print(index .. ") " .. fail.testName)2434print(fail.msg)2435print(fail.stackTrace)2436print()2437end24382439function TextOutput:displayErroredTests()2440if #self.result.errorTests ~= 0 then2441print("Tests with errors:")2442print("------------------")2443for i, v in ipairs(self.result.errorTests) do2444self:displayOneFailedTest(i, v)2445end2446end2447end24482449function TextOutput:displayFailedTests()2450if #self.result.failedTests ~= 0 then2451print("Failed tests:")2452print("-------------")2453for i, v in ipairs(self.result.failedTests) do2454self:displayOneFailedTest(i, v)2455end2456end2457end24582459function TextOutput:endSuite()2460if self.verbosity > M.VERBOSITY_DEFAULT then2461print("=========================================================")2462else2463print()2464end2465self:displayErroredTests()2466self:displayFailedTests()2467print(M.LuaUnit.statusLine(self.result))2468if self.result.notSuccessCount == 0 then2469print('OK')2470end2471end24722473-- class TextOutput end247424752476----------------------------------------------------------------2477-- class NilOutput2478----------------------------------------------------------------24792480local function nopCallable()2481--print(42)2482return nopCallable2483end24842485local NilOutput = { __class__ = 'NilOuptut' } -- class2486local NilOutput_MT = { __index = nopCallable } -- metatable24872488function NilOutput.new(runner)2489return setmetatable({ __class__ = 'NilOutput' }, NilOutput_MT)2490end24912492----------------------------------------------------------------2493--2494-- class LuaUnit2495--2496----------------------------------------------------------------24972498M.LuaUnit = {2499outputType = TextOutput,2500verbosity = M.VERBOSITY_DEFAULT,2501__class__ = 'LuaUnit',2502instances = {}2503}2504local LuaUnit_MT = { __index = M.LuaUnit }25052506if EXPORT_ASSERT_TO_GLOBALS then2507LuaUnit = M.LuaUnit2508end25092510function M.LuaUnit.new()2511local newInstance = setmetatable({}, LuaUnit_MT)2512return newInstance2513end25142515-----------------[[ Utility methods ]]---------------------25162517function M.LuaUnit.asFunction(aObject)2518-- return "aObject" if it is a function, and nil otherwise2519if 'function' == type(aObject) then2520return aObject2521end2522end25232524function M.LuaUnit.splitClassMethod(someName)2525--[[2526Return a pair of class_name, methodName strings for a name in the form2527"class.method". If no class part (or separator) is found, will return2528nil, someName instead (the latter being unchanged).25292530This convention thus also replaces the older isClassMethod() test:2531You just have to check for a non-nil class_name (return) value.2532]]2533local separator = string.find(someName, '.', 1, true)2534if separator then2535return someName:sub(1, separator - 1), someName:sub(separator + 1)2536end2537return nil, someName2538end25392540function M.LuaUnit.isMethodTestName(s)2541-- return true is the name matches the name of a test method2542-- default rule is that is starts with 'Test' or with 'test'2543return string.sub(s, 1, 4):lower() == 'test'2544end25452546function M.LuaUnit.isTestName(s)2547-- return true is the name matches the name of a test2548-- default rule is that is starts with 'Test' or with 'test'2549return string.sub(s, 1, 4):lower() == 'test'2550end25512552function M.LuaUnit.collectTests()2553-- return a list of all test names in the global namespace2554-- that match LuaUnit.isTestName25552556local testNames = {}2557for k, _ in pairs(_G) do2558if type(k) == "string" and M.LuaUnit.isTestName(k) then2559table.insert(testNames, k)2560end2561end2562table.sort(testNames)2563return testNames2564end25652566function M.LuaUnit.parseCmdLine(cmdLine)2567-- parse the command line2568-- Supported command line parameters:2569-- --verbose, -v: increase verbosity2570-- --quiet, -q: silence output2571-- --error, -e: treat errors as fatal (quit program)2572-- --output, -o, + name: select output type2573-- --pattern, -p, + pattern: run test matching pattern, may be repeated2574-- --exclude, -x, + pattern: run test not matching pattern, may be repeated2575-- --shuffle, -s, : shuffle tests before reunning them2576-- --name, -n, + fname: name of output file for junit, default to stdout2577-- --repeat, -r, + num: number of times to execute each test2578-- [testnames, ...]: run selected test names2579--2580-- Returns a table with the following fields:2581-- verbosity: nil, M.VERBOSITY_DEFAULT, M.VERBOSITY_QUIET, M.VERBOSITY_VERBOSE2582-- output: nil, 'tap', 'junit', 'text', 'nil'2583-- testNames: nil or a list of test names to run2584-- exeRepeat: num or 12585-- pattern: nil or a list of patterns2586-- exclude: nil or a list of patterns25872588local result, state = {}, nil2589local SET_OUTPUT = 12590local SET_PATTERN = 22591local SET_EXCLUDE = 32592local SET_FNAME = 42593local SET_REPEAT = 525942595if cmdLine == nil then2596return result2597end25982599local function parseOption(option)2600if option == '--help' or option == '-h' then2601result['help'] = true2602return2603elseif option == '--version' then2604result['version'] = true2605return2606elseif option == '--verbose' or option == '-v' then2607result['verbosity'] = M.VERBOSITY_VERBOSE2608return2609elseif option == '--quiet' or option == '-q' then2610result['verbosity'] = M.VERBOSITY_QUIET2611return2612elseif option == '--error' or option == '-e' then2613result['quitOnError'] = true2614return2615elseif option == '--failure' or option == '-f' then2616result['quitOnFailure'] = true2617return2618elseif option == '--shuffle' or option == '-s' then2619result['shuffle'] = true2620return2621elseif option == '--output' or option == '-o' then2622state = SET_OUTPUT2623return state2624elseif option == '--name' or option == '-n' then2625state = SET_FNAME2626return state2627elseif option == '--repeat' or option == '-r' then2628state = SET_REPEAT2629return state2630elseif option == '--pattern' or option == '-p' then2631state = SET_PATTERN2632return state2633elseif option == '--exclude' or option == '-x' then2634state = SET_EXCLUDE2635return state2636end2637error('Unknown option: ' .. option, 3)2638end26392640local function setArg(cmdArg, state)2641if state == SET_OUTPUT then2642result['output'] = cmdArg2643return2644elseif state == SET_FNAME then2645result['fname'] = cmdArg2646return2647elseif state == SET_REPEAT then2648result['exeRepeat'] = tonumber(cmdArg)2649or error('Malformed -r argument: ' .. cmdArg)2650return2651elseif state == SET_PATTERN then2652if result['pattern'] then2653table.insert(result['pattern'], cmdArg)2654else2655result['pattern'] = { cmdArg }2656end2657return2658elseif state == SET_EXCLUDE then2659local notArg = '!' .. cmdArg2660if result['pattern'] then2661table.insert(result['pattern'], notArg)2662else2663result['pattern'] = { notArg }2664end2665return2666end2667error('Unknown parse state: ' .. state)2668end26692670for i, cmdArg in ipairs(cmdLine) do2671if state ~= nil then2672setArg(cmdArg, state, result)2673state = nil2674else2675if cmdArg:sub(1, 1) == '-' then2676state = parseOption(cmdArg)2677else2678if result['testNames'] then2679table.insert(result['testNames'], cmdArg)2680else2681result['testNames'] = { cmdArg }2682end2683end2684end2685end26862687if result['help'] then2688M.LuaUnit.help()2689end26902691if result['version'] then2692M.LuaUnit.version()2693end26942695if state ~= nil then2696error('Missing argument after ' .. cmdLine[#cmdLine], 2)2697end26982699return result2700end27012702function M.LuaUnit.help()2703print(M.USAGE)2704os.exit(0)2705end27062707function M.LuaUnit.version()2708print('LuaUnit v' .. M.VERSION .. ' by Philippe Fremy <[email protected]>')2709os.exit(0)2710end27112712----------------------------------------------------------------2713-- class NodeStatus2714----------------------------------------------------------------27152716local NodeStatus = { __class__ = 'NodeStatus' } -- class2717local NodeStatus_MT = { __index = NodeStatus } -- metatable2718M.NodeStatus = NodeStatus27192720-- values of status2721NodeStatus.SUCCESS = 'SUCCESS'2722NodeStatus.SKIP = 'SKIP'2723NodeStatus.FAIL = 'FAIL'2724NodeStatus.ERROR = 'ERROR'27252726function NodeStatus.new(number, testName, class_name)2727-- default constructor, test are PASS by default2728local t = { number = number, testName = testName, class_name = class_name }2729setmetatable(t, NodeStatus_MT)2730t:success()2731return t2732end27332734function NodeStatus:success()2735self.status = self.SUCCESS2736-- useless because lua does this for us, but it helps me remembering the relevant field names2737self.msg = nil2738self.stackTrace = nil2739end27402741function NodeStatus:skip(msg)2742self.status = self.SKIP2743self.msg = msg2744self.stackTrace = nil2745end27462747function NodeStatus:fail(msg, stackTrace)2748self.status = self.FAIL2749self.msg = msg2750self.stackTrace = stackTrace2751end27522753function NodeStatus:error(msg, stackTrace)2754self.status = self.ERROR2755self.msg = msg2756self.stackTrace = stackTrace2757end27582759function NodeStatus:isSuccess()2760return self.status == NodeStatus.SUCCESS2761end27622763function NodeStatus:isNotSuccess()2764-- Return true if node is either failure or error or skip2765return (self.status == NodeStatus.FAIL or self.status == NodeStatus.ERROR or self.status == NodeStatus.SKIP)2766end27672768function NodeStatus:isSkipped()2769return self.status == NodeStatus.SKIP2770end27712772function NodeStatus:isFailure()2773return self.status == NodeStatus.FAIL2774end27752776function NodeStatus:isError()2777return self.status == NodeStatus.ERROR2778end27792780function NodeStatus:statusXML()2781if self:isError() then2782return table.concat(2783{ ' <error type="', xmlEscape(self.msg), '">\n',2784' <![CDATA[', xmlCDataEscape(self.stackTrace),2785']]></error>\n' })2786elseif self:isFailure() then2787return table.concat(2788{ ' <failure type="', xmlEscape(self.msg), '">\n',2789' <![CDATA[', xmlCDataEscape(self.stackTrace),2790']]></failure>\n' })2791elseif self:isSkipped() then2792return table.concat({ ' <skipped>', xmlEscape(self.msg), '</skipped>\n' })2793end2794return ' <passed/>\n' -- (not XSD-compliant! normally shouldn't get here)2795end27962797--------------[[ Output methods ]]-------------------------27982799local function conditional_plural(number, singular)2800-- returns a grammatically well-formed string "%d <singular/plural>"2801local suffix = ''2802if number ~= 1 then2803-- use plural2804suffix = (singular:sub(-2) == 'ss') and 'es' or 's'2805end2806return string.format('%d %s%s', number, singular, suffix)2807end28082809function M.LuaUnit.statusLine(result)2810-- return status line string according to results2811local s = {2812string.format('Ran %d tests in %0.3f seconds',2813result.runCount, result.duration),2814conditional_plural(result.successCount, 'success'),2815}2816if result.notSuccessCount > 0 then2817if result.failureCount > 0 then2818table.insert(s, conditional_plural(result.failureCount, 'failure'))2819end2820if result.errorCount > 0 then2821table.insert(s, conditional_plural(result.errorCount, 'error'))2822end2823else2824table.insert(s, '0 failures')2825end2826if result.skippedCount > 0 then2827table.insert(s, string.format("%d skipped", result.skippedCount))2828end2829if result.nonSelectedCount > 0 then2830table.insert(s, string.format("%d non-selected", result.nonSelectedCount))2831end2832return table.concat(s, ', ')2833end28342835function M.LuaUnit:startSuite(selectedCount, nonSelectedCount)2836self.result = {2837selectedCount = selectedCount,2838nonSelectedCount = nonSelectedCount,2839successCount = 0,2840runCount = 0,2841currentTestNumber = 0,2842currentclass_name = "",2843currentNode = nil,2844suiteStarted = true,2845startTime = os.clock(),2846startDate = os.date(os.getenv('LUAUNIT_DATEFMT')),2847startIsodate = os.date('%Y-%m-%dT%H:%M:%S'),2848patternIncludeFilter = self.patternIncludeFilter,28492850-- list of test node status2851allTests = {},2852failedTests = {},2853errorTests = {},2854skippedTests = {},28552856failureCount = 0,2857errorCount = 0,2858notSuccessCount = 0,2859skippedCount = 0,2860}28612862self.outputType = self.outputType or TextOutput2863self.output = self.outputType.new(self)2864self.output:startSuite()2865end28662867function M.LuaUnit:startClass(class_name, classInstance)2868self.result.currentclass_name = class_name2869self.output:startClass(class_name)2870self:setupClass(class_name, classInstance)2871end28722873function M.LuaUnit:startTest(testName)2874self.result.currentTestNumber = self.result.currentTestNumber + 12875self.result.runCount = self.result.runCount + 12876self.result.currentNode = NodeStatus.new(2877self.result.currentTestNumber,2878testName,2879self.result.currentclass_name2880)2881self.result.currentNode.startTime = os.clock()2882table.insert(self.result.allTests, self.result.currentNode)2883self.output:startTest(testName)2884end28852886function M.LuaUnit:updateStatus(err)2887-- "err" is expected to be a table / result from protectedCall()2888if err.status == NodeStatus.SUCCESS then2889return2890end28912892local node = self.result.currentNode28932894--[[ As a first approach, we will report only one error or one failure for one test.28952896However, we can have the case where the test is in failure, and the teardown is in error.2897In such case, it's a good idea to report both a failure and an error in the test suite. This is2898what Python unittest does for example. However, it mixes up counts so need to be handled carefully: for2899example, there could be more (failures + errors) count that tests. What happens to the current node ?29002901We will do this more intelligent version later.2902]]29032904-- if the node is already in failure/error, just don't report the new error (see above)2905if node.status ~= NodeStatus.SUCCESS then2906return2907end29082909if err.status == NodeStatus.FAIL then2910node:fail(err.msg, err.trace)2911table.insert(self.result.failedTests, node)2912elseif err.status == NodeStatus.ERROR then2913node:error(err.msg, err.trace)2914table.insert(self.result.errorTests, node)2915elseif err.status == NodeStatus.SKIP then2916node:skip(err.msg)2917table.insert(self.result.skippedTests, node)2918else2919error('No such status: ' .. prettystr(err.status))2920end29212922self.output:updateStatus(node)2923end29242925function M.LuaUnit:endTest()2926local node = self.result.currentNode2927-- print( 'endTest() '..prettystr(node))2928-- print( 'endTest() '..prettystr(node:isNotSuccess()))2929node.duration = os.clock() - node.startTime2930node.startTime = nil2931self.output:endTest(node)29322933if node:isSuccess() then2934self.result.successCount = self.result.successCount + 12935elseif node:isError() then2936if self.quitOnError or self.quitOnFailure then2937-- Runtime error - abort test execution as requested by2938-- "--error" option. This is done by setting a special2939-- flag that gets handled in internalRunSuiteByInstances().2940print("\nERROR during LuaUnit test execution:\n" .. node.msg)2941self.result.aborted = true2942end2943elseif node:isFailure() then2944if self.quitOnFailure then2945-- Failure - abort test execution as requested by2946-- "--failure" option. This is done by setting a special2947-- flag that gets handled in internalRunSuiteByInstances().2948print("\nFailure during LuaUnit test execution:\n" .. node.msg)2949self.result.aborted = true2950end2951elseif node:isSkipped() then2952self.result.runCount = self.result.runCount - 12953else2954error('No such node status: ' .. prettystr(node.status))2955end2956self.result.currentNode = nil2957end29582959function M.LuaUnit:endClass()2960self:teardownClass(self.lastclass_name, self.lastClassInstance)2961self.output:endClass()2962end29632964function M.LuaUnit:endSuite()2965if self.result.suiteStarted == false then2966error('LuaUnit:endSuite() -- suite was already ended')2967end2968self.result.duration = os.clock() - self.result.startTime2969self.result.suiteStarted = false29702971-- Expose test counts for outputter's endSuite(). This could be managed2972-- internally instead by using the length of the lists of failed tests2973-- but unit tests rely on these fields being present.2974self.result.failureCount = #self.result.failedTests2975self.result.errorCount = #self.result.errorTests2976self.result.notSuccessCount = self.result.failureCount + self.result.errorCount2977self.result.skippedCount = #self.result.skippedTests29782979self.output:endSuite()2980end29812982function M.LuaUnit:setOutputType(outputType, fname)2983-- Configures LuaUnit runner output2984-- outputType is one of: NIL, TAP, JUNIT, TEXT2985-- when outputType is junit, the additional argument fname is used to set the name of junit output file2986-- for other formats, fname is ignored2987if outputType:upper() == "NIL" then2988self.outputType = NilOutput2989return2990end2991if outputType:upper() == "TAP" then2992self.outputType = TapOutput2993return2994end2995if outputType:upper() == "JUNIT" then2996self.outputType = JUnitOutput2997if fname then2998self.fname = fname2999end3000return3001end3002if outputType:upper() == "TEXT" then3003self.outputType = TextOutput3004return3005end3006error('No such format: ' .. outputType, 2)3007end30083009--------------[[ Runner ]]-----------------30103011function M.LuaUnit:protectedCall(classInstance, methodInstance, prettyFuncName)3012-- if classInstance is nil, this is just a function call3013-- else, it's method of a class being called.30143015local function err_handler(e)3016-- transform error into a table, adding the traceback information3017return {3018status = NodeStatus.ERROR,3019msg = e,3020trace = string.sub(debug.traceback("", 1), 2)3021}3022end30233024local ok, err3025if classInstance then3026-- stupid Lua < 5.2 does not allow xpcall with arguments so let's use a workaround3027ok, err = xpcall(function()3028methodInstance(classInstance)3029end, err_handler)3030else3031ok, err = xpcall(function()3032methodInstance()3033end, err_handler)3034end3035if ok then3036return { status = NodeStatus.SUCCESS }3037end3038-- print('ok="'..prettystr(ok)..'" err="'..prettystr(err)..'"')30393040local iter_msg3041iter_msg = self.exeRepeat and 'iteration ' .. self.currentCount30423043err.msg, err.status = M.adjust_err_msg_with_iter(err.msg, iter_msg)30443045if err.status == NodeStatus.SUCCESS or err.status == NodeStatus.SKIP then3046err.trace = nil3047return err3048end30493050-- reformat / improve the stack trace3051if prettyFuncName then3052-- we do have the real method name3053err.trace = err.trace:gsub("in (%a+) 'methodInstance'", "in %1 '" .. prettyFuncName .. "'")3054end3055if STRIP_LUAUNIT_FROM_STACKTRACE then3056err.trace = stripLuaunitTrace2(err.trace, err.msg)3057end30583059return err -- return the error "object" (table)3060end30613062function M.LuaUnit:execOneFunction(class_name, methodName, classInstance, methodInstance)3063-- When executing a test function, class_name and classInstance must be nil3064-- When executing a class method, all parameters must be set30653066if type(methodInstance) ~= 'function' then3067self:unregisterSuite()3068error(tostring(methodName) .. ' must be a function, not ' .. type(methodInstance))3069end30703071local prettyFuncName3072if class_name == nil then3073class_name = '[TestFunctions]'3074prettyFuncName = methodName3075else3076prettyFuncName = class_name .. '.' .. methodName3077end30783079if self.lastclass_name ~= class_name then3080if self.lastclass_name ~= nil then3081self:endClass()3082end3083self:startClass(class_name, classInstance)3084self.lastclass_name = class_name3085self.lastClassInstance = classInstance3086end30873088self:startTest(prettyFuncName)30893090local node = self.result.currentNode3091for iter_n = 1, self.exeRepeat or 1 do3092if node:isNotSuccess() then3093break3094end3095self.currentCount = iter_n30963097-- run setUp first (if any)3098if classInstance then3099local func = self.asFunction(classInstance.setUp) or3100self.asFunction(classInstance.Setup) or3101self.asFunction(classInstance.setup) or3102self.asFunction(classInstance.SetUp)3103if func then3104self:updateStatus(self:protectedCall(classInstance, func, class_name .. '.setUp'))3105end3106end31073108-- run testMethod()3109if node:isSuccess() then3110self:updateStatus(self:protectedCall(classInstance, methodInstance, prettyFuncName))3111end31123113-- lastly, run tearDown (if any)3114if classInstance then3115local func = self.asFunction(classInstance.tearDown) or3116self.asFunction(classInstance.TearDown) or3117self.asFunction(classInstance.teardown) or3118self.asFunction(classInstance.Teardown)3119if func then3120self:updateStatus(self:protectedCall(classInstance, func, class_name .. '.tearDown'))3121end3122end3123end31243125self:endTest()3126end31273128function M.LuaUnit.expandOneClass(result, class_name, classInstance)3129--[[3130Input: a list of { name, instance }, a class name, a class instance3131Ouptut: modify result to add all test method instance in the form:3132{ class_name.methodName, classInstance }3133]]3134for methodName, methodInstance in sortedPairs(classInstance) do3135if M.LuaUnit.asFunction(methodInstance) and M.LuaUnit.isMethodTestName(methodName) then3136table.insert(result, { class_name .. '.' .. methodName, classInstance })3137end3138end3139end31403141function M.LuaUnit.expandClasses(listOfNameAndInst)3142--[[3143-- expand all classes (provided as {class_name, classInstance}) to a list of {class_name.methodName, classInstance}3144-- functions and methods remain untouched31453146Input: a list of { name, instance }31473148Output:3149* { function name, function instance } : do nothing3150* { class.method name, class instance }: do nothing3151* { class name, class instance } : add all method names in the form of (class_name.methodName, classInstance)3152]]3153local result = {}31543155for i, v in ipairs(listOfNameAndInst) do3156local name, instance = v[1], v[2]3157if M.LuaUnit.asFunction(instance) then3158table.insert(result, { name, instance })3159else3160if type(instance) ~= 'table' then3161error('Instance must be a table or a function, not a ' .. type(instance) .. ' with value ' .. prettystr(instance))3162end3163local class_name, methodName = M.LuaUnit.splitClassMethod(name)3164if class_name then3165local methodInstance = instance[methodName]3166if methodInstance == nil then3167error("Could not find method in class " .. tostring(class_name) .. " for method " .. tostring(methodName))3168end3169table.insert(result, { name, instance })3170else3171M.LuaUnit.expandOneClass(result, name, instance)3172end3173end3174end31753176return result3177end31783179function M.LuaUnit.applyPatternFilter(patternIncFilter, listOfNameAndInst)3180local included, excluded = {}, {}3181for i, v in ipairs(listOfNameAndInst) do3182-- local name, instance = v[1], v[2]3183if patternFilter(patternIncFilter, v[1]) then3184table.insert(included, v)3185else3186table.insert(excluded, v)3187end3188end3189return included, excluded3190end31913192local function getKeyInListWithGlobalFallback(key, listOfNameAndInst)3193local result = nil3194for i, v in ipairs(listOfNameAndInst) do3195if (listOfNameAndInst[i][1] == key) then3196result = listOfNameAndInst[i][2]3197break3198end3199end3200if (not M.LuaUnit.asFunction(result)) then3201result = _G[key]3202end3203return result3204end32053206function M.LuaUnit:setupSuite(listOfNameAndInst)3207local setupSuite = getKeyInListWithGlobalFallback("setupSuite", listOfNameAndInst)3208if self.asFunction(setupSuite) then3209self:updateStatus(self:protectedCall(nil, setupSuite, 'setupSuite'))3210end3211end32123213function M.LuaUnit:teardownSuite(listOfNameAndInst)3214local teardownSuite = getKeyInListWithGlobalFallback("teardownSuite", listOfNameAndInst)3215if self.asFunction(teardownSuite) then3216self:updateStatus(self:protectedCall(nil, teardownSuite, 'teardownSuite'))3217end3218end32193220function M.LuaUnit:setupClass(class_name, instance)3221if type(instance) == 'table' and self.asFunction(instance.setupClass) then3222self:updateStatus(self:protectedCall(instance, instance.setupClass, class_name .. '.setupClass'))3223end3224end32253226function M.LuaUnit:teardownClass(class_name, instance)3227if type(instance) == 'table' and self.asFunction(instance.teardownClass) then3228self:updateStatus(self:protectedCall(instance, instance.teardownClass, class_name .. '.teardownClass'))3229end3230end32313232function M.LuaUnit:internalRunSuiteByInstances(listOfNameAndInst)3233--[[ Run an explicit list of tests. Each item of the list must be one of:3234* { function name, function instance }3235* { class name, class instance }3236* { class.method name, class instance }32373238This function is internal to LuaUnit. The official API to perform this action is runSuiteByInstances()3239]]32403241local expandedList = self.expandClasses(listOfNameAndInst)3242if self.shuffle then3243randomizeTable(expandedList)3244end3245local filteredList, filteredOutList = self.applyPatternFilter(3246self.patternIncludeFilter, expandedList)32473248self:startSuite(#filteredList, #filteredOutList)3249self:setupSuite(listOfNameAndInst)32503251for i, v in ipairs(filteredList) do3252local name, instance = v[1], v[2]3253if M.LuaUnit.asFunction(instance) then3254self:execOneFunction(nil, name, nil, instance)3255else3256-- expandClasses() should have already taken care of sanitizing the input3257assert(type(instance) == 'table')3258local class_name, methodName = M.LuaUnit.splitClassMethod(name)3259assert(class_name ~= nil)3260local methodInstance = instance[methodName]3261assert(methodInstance ~= nil)3262self:execOneFunction(class_name, methodName, instance, methodInstance)3263end3264if self.result.aborted then3265break -- "--error" or "--failure" option triggered3266end3267end32683269if self.lastclass_name ~= nil then3270self:endClass()3271end32723273self:teardownSuite(listOfNameAndInst)3274self:endSuite()32753276if self.result.aborted then3277print("LuaUnit ABORTED (as requested by --error or --failure option)")3278self:unregisterSuite()3279os.exit(-2)3280end3281end32823283function M.LuaUnit:internalRunSuiteByNames(listOfName)3284--[[ Run LuaUnit with a list of generic names, coming either from command-line or from global3285namespace analysis. Convert the list into a list of (name, valid instances (table or function))3286and calls internalRunSuiteByInstances.3287]]32883289local instanceName, instance3290local listOfNameAndInst = {}32913292for i, name in ipairs(listOfName) do3293local class_name, methodName = M.LuaUnit.splitClassMethod(name)3294if class_name then3295instanceName = class_name3296instance = _G[instanceName]32973298if instance == nil then3299self:unregisterSuite()3300error("No such name in global space: " .. instanceName)3301end33023303if type(instance) ~= 'table' then3304self:unregisterSuite()3305error('Instance of ' .. instanceName .. ' must be a table, not ' .. type(instance))3306end33073308local methodInstance = instance[methodName]3309if methodInstance == nil then3310self:unregisterSuite()3311error("Could not find method in class " .. tostring(class_name) .. " for method " .. tostring(methodName))3312end33133314else3315-- for functions and classes3316instanceName = name3317instance = _G[instanceName]3318end33193320if instance == nil then3321self:unregisterSuite()3322error("No such name in global space: " .. instanceName)3323end33243325if (type(instance) ~= 'table' and type(instance) ~= 'function') then3326self:unregisterSuite()3327error('Name must match a function or a table: ' .. instanceName)3328end33293330table.insert(listOfNameAndInst, { name, instance })3331end33323333self:internalRunSuiteByInstances(listOfNameAndInst)3334end33353336function M.LuaUnit.run(...)3337-- Run some specific test classes.3338-- If no arguments are passed, run the class names specified on the3339-- command line. If no class name is specified on the command line3340-- run all classes whose name starts with 'Test'3341--3342-- If arguments are passed, they must be strings of the class names3343-- that you want to run or generic command line arguments (-o, -p, -v, ...)3344local runner = M.LuaUnit.new()3345return runner:runSuite(...)3346end33473348function M.LuaUnit:registerSuite()3349-- register the current instance into our global array of instances3350-- print('-> Register suite')3351M.LuaUnit.instances[#M.LuaUnit.instances + 1] = self3352end33533354function M.unregisterCurrentSuite()3355-- force unregister the last registered suite3356table.remove(M.LuaUnit.instances, #M.LuaUnit.instances)3357end33583359function M.LuaUnit:unregisterSuite()3360-- print('<- Unregister suite')3361-- remove our current instqances from the global array of instances3362local instanceIdx = nil3363for i, instance in ipairs(M.LuaUnit.instances) do3364if instance == self then3365instanceIdx = i3366break3367end3368end33693370if instanceIdx ~= nil then3371table.remove(M.LuaUnit.instances, instanceIdx)3372-- print('Unregister done')3373end33743375end33763377function M.LuaUnit:initFromArguments(...)3378--[[Parses all arguments from either command-line or direct call and set internal3379flags of LuaUnit runner according to it.33803381Return the list of names which were possibly passed on the command-line or as arguments3382]]3383local args = { ... }3384if type(args[1]) == 'table' and args[1].__class__ == 'LuaUnit' then3385-- run was called with the syntax M.LuaUnit:runSuite()3386-- we support both M.LuaUnit.run() and M.LuaUnit:run()3387-- strip out the first argument self to make it a command-line argument list3388table.remove(args, 1)3389end33903391if #args == 0 then3392args = cmdline_argv3393end33943395local options = pcall_or_abort(M.LuaUnit.parseCmdLine, args)33963397-- We expect these option fields to be either `nil` or contain3398-- valid values, so it's safe to always copy them directly.3399self.verbosity = options.verbosity3400self.quitOnError = options.quitOnError3401self.quitOnFailure = options.quitOnFailure34023403self.exeRepeat = options.exeRepeat3404self.patternIncludeFilter = options.pattern3405self.shuffle = options.shuffle34063407options.output = options.output or os.getenv('LUAUNIT_OUTPUT')3408options.fname = options.fname or os.getenv('LUAUNIT_JUNIT_FNAME')34093410if options.output then3411if options.output:lower() == 'junit' and options.fname == nil then3412print('With junit output, a filename must be supplied with -n or --name')3413os.exit(-1)3414end3415pcall_or_abort(self.setOutputType, self, options.output, options.fname)3416end34173418return options.testNames3419end34203421function M.LuaUnit:runSuite(...)3422testNames = self:initFromArguments(...)3423self:registerSuite()3424self:internalRunSuiteByNames(testNames or M.LuaUnit.collectTests())3425self:unregisterSuite()3426return self.result.notSuccessCount3427end34283429function M.LuaUnit:runSuiteByInstances(listOfNameAndInst, commandLineArguments)3430--[[3431Run all test functions or tables provided as input.34323433Input: a list of { name, instance }3434instance can either be a function or a table containing test functions starting with the prefix "test"34353436return the number of failures and errors, 0 meaning success3437]]3438-- parse the command-line arguments3439testNames = self:initFromArguments(commandLineArguments)3440self:registerSuite()3441self:internalRunSuiteByInstances(listOfNameAndInst)3442self:unregisterSuite()3443return self.result.notSuccessCount3444end3445344634473448-- class LuaUnit34493450-- For compatbility with LuaUnit v23451M.run = M.LuaUnit.run3452M.Run = M.LuaUnit.run34533454function M:setVerbosity(verbosity)3455-- set the verbosity value (as integer)3456M.LuaUnit.verbosity = verbosity3457end3458M.set_verbosity = M.setVerbosity3459M.SetVerbosity = M.setVerbosity34603461return M3462346334643465