| --[[ |
| luaunit.lua |
| |
| Description: A unit testing framework |
| Homepage: https://github.com/bluebird75/luaunit |
| Development by Philippe Fremy <phil@freehackers.org> |
| Based on initial work of Ryu, Gwang (http://www.gpgstudy.com/gpgiki/LuaUnit) |
| License: BSD License, see LICENSE.txt |
| Version: 3.2 |
| ]]-- |
| |
| require("math") |
| local M={} |
| |
| -- private exported functions (for testing) |
| M.private = {} |
| |
| M.VERSION='3.2' |
| |
| --[[ Some people like assertEquals( actual, expected ) and some people prefer |
| assertEquals( expected, actual ). |
| ]]-- |
| M.ORDER_ACTUAL_EXPECTED = true |
| M.PRINT_TABLE_REF_IN_ERROR_MSG = false |
| M.TABLE_EQUALS_KEYBYCONTENT = true |
| M.LINE_LENGTH=80 |
| |
| -- set this to false to debug luaunit |
| local STRIP_LUAUNIT_FROM_STACKTRACE=true |
| |
| M.VERBOSITY_DEFAULT = 10 |
| M.VERBOSITY_LOW = 1 |
| M.VERBOSITY_QUIET = 0 |
| M.VERBOSITY_VERBOSE = 20 |
| |
| -- set EXPORT_ASSERT_TO_GLOBALS to have all asserts visible as global values |
| -- EXPORT_ASSERT_TO_GLOBALS = true |
| |
| -- we need to keep a copy of the script args before it is overridden |
| local cmdline_argv = rawget(_G, "arg") |
| |
| M.FAILURE_PREFIX = 'LuaUnit test FAILURE: ' -- prefix string for failed tests |
| |
| M.USAGE=[[Usage: lua <your_test_suite.lua> [options] [testname1 [testname2] ... ] |
| Options: |
| -h, --help: Print this help |
| --version: Print version information |
| -v, --verbose: Increase verbosity |
| -q, --quiet: Set verbosity to minimum |
| -e, --error: Stop on first error |
| -f, --failure: Stop on first failure or error |
| -o, --output OUTPUT: Set output type to OUTPUT |
| Possible values: text, tap, junit, nil |
| -n, --name NAME: For junit only, mandatory name of xml file |
| -p, --pattern PATTERN: Execute all test names matching the Lua PATTERN |
| May be repeated to include severals patterns |
| Make sure you escape magic chars like +? with % |
| testname1, testname2, ... : tests to run in the form of testFunction, |
| TestClass or TestClass.testMethod |
| ]] |
| |
| ---------------------------------------------------------------- |
| -- |
| -- general utility functions |
| -- |
| ---------------------------------------------------------------- |
| |
| local crossTypeOrdering = { |
| number = 1, |
| boolean = 2, |
| string = 3, |
| table = 4, |
| other = 5 |
| } |
| local crossTypeComparison = { |
| number = function(a, b) return a < b end, |
| string = function(a, b) return a < b end, |
| other = function(a, b) return tostring(a) < tostring(b) end, |
| } |
| |
| local function crossTypeSort(a, b) |
| local type_a, type_b = type(a), type(b) |
| if type_a == type_b then |
| local func = crossTypeComparison[type_a] or crossTypeComparison.other |
| return func(a, b) |
| end |
| type_a = crossTypeOrdering[type_a] or crossTypeOrdering.other |
| type_b = crossTypeOrdering[type_b] or crossTypeOrdering.other |
| return type_a < type_b |
| end |
| |
| local function __genSortedIndex( t ) |
| -- Returns a sequence consisting of t's keys, sorted. |
| local sortedIndex = {} |
| |
| for key,_ in pairs(t) do |
| table.insert(sortedIndex, key) |
| end |
| |
| table.sort(sortedIndex, crossTypeSort) |
| return sortedIndex |
| end |
| M.private.__genSortedIndex = __genSortedIndex |
| |
| local function sortedNext(state, control) |
| -- Equivalent of the next() function of table iteration, but returns the |
| -- keys in sorted order (see __genSortedIndex and crossTypeSort). |
| -- The state is a temporary variable during iteration and contains the |
| -- sorted key table (state.sortedIdx). It also stores the last index (into |
| -- the keys) used by the iteration, to find the next one quickly. |
| local key |
| |
| --print("sortedNext: control = "..tostring(control) ) |
| if control == nil then |
| -- start of iteration |
| state.lastIdx = 1 |
| key = state.sortedIdx[1] |
| return key, state.t[key] |
| end |
| |
| -- normally, we expect the control variable to match the last key used |
| if control ~= state.sortedIdx[state.lastIdx] then |
| -- strange, we have to find the next value by ourselves |
| -- the key table is sorted in crossTypeSort() order! -> use bisection |
| local count = #state.sortedIdx |
| local lower, upper = 1, count |
| repeat |
| state.lastIdx = math.modf((lower + upper) / 2) |
| key = state.sortedIdx[state.lastIdx] |
| if key == control then break; end -- key found (and thus prev index) |
| if crossTypeSort(key, control) then |
| -- key < control, continue search "right" (towards upper bound) |
| lower = state.lastIdx + 1 |
| else |
| -- key > control, continue search "left" (towards lower bound) |
| upper = state.lastIdx - 1 |
| end |
| until lower > upper |
| if lower > upper then -- only true if the key wasn't found, ... |
| state.lastIdx = count -- ... so ensure no match for the code below |
| end |
| end |
| |
| -- proceed by retrieving the next value (or nil) from the sorted keys |
| state.lastIdx = state.lastIdx + 1 |
| key = state.sortedIdx[state.lastIdx] |
| if key then |
| return key, state.t[key] |
| end |
| |
| -- getting here means returning `nil`, which will end the iteration |
| end |
| |
| local function sortedPairs(tbl) |
| -- Equivalent of the pairs() function on tables. Allows to iterate in |
| -- sorted order. As required by "generic for" loops, this will return the |
| -- iterator (function), an "invariant state", and the initial control value. |
| -- (see http://www.lua.org/pil/7.2.html) |
| return sortedNext, {t = tbl, sortedIdx = __genSortedIndex(tbl)}, nil |
| end |
| M.private.sortedPairs = sortedPairs |
| |
| local function strsplit(delimiter, text) |
| -- Split text into a list consisting of the strings in text, |
| -- separated by strings matching delimiter (which may be a pattern). |
| -- example: strsplit(",%s*", "Anna, Bob, Charlie,Dolores") |
| if string.find("", delimiter, 1, true) then -- this would result in endless loops |
| error("delimiter matches empty string!") |
| end |
| local list, pos, first, last = {}, 1 |
| while true do |
| first, last = text:find(delimiter, pos, true) |
| if first then -- found? |
| table.insert(list, text:sub(pos, first - 1)) |
| pos = last + 1 |
| else |
| table.insert(list, text:sub(pos)) |
| break |
| end |
| end |
| return list |
| end |
| M.private.strsplit = strsplit |
| |
| local function hasNewLine( s ) |
| -- return true if s has a newline |
| return (string.find(s, '\n', 1, true) ~= nil) |
| end |
| M.private.hasNewLine = hasNewLine |
| |
| local function prefixString( prefix, s ) |
| -- Prefix all the lines of s with prefix |
| return prefix .. table.concat(strsplit('\n', s), '\n' .. prefix) |
| end |
| M.private.prefixString = prefixString |
| |
| local function strMatch(s, pattern, start, final ) |
| -- return true if s matches completely the pattern from index start to index end |
| -- return false in every other cases |
| -- if start is nil, matches from the beginning of the string |
| -- if final is nil, matches to the end of the string |
| start = start or 1 |
| final = final or string.len(s) |
| |
| local foundStart, foundEnd = string.find(s, pattern, start, false) |
| return foundStart == start and foundEnd == final |
| end |
| M.private.strMatch = strMatch |
| |
| local function xmlEscape( s ) |
| -- Return s escaped for XML attributes |
| -- escapes table: |
| -- " " |
| -- ' ' |
| -- < < |
| -- > > |
| -- & & |
| |
| return string.gsub( s, '.', { |
| ['&'] = "&", |
| ['"'] = """, |
| ["'"] = "'", |
| ['<'] = "<", |
| ['>'] = ">", |
| } ) |
| end |
| M.private.xmlEscape = xmlEscape |
| |
| local function xmlCDataEscape( s ) |
| -- Return s escaped for CData section, escapes: "]]>" |
| return string.gsub( s, ']]>', ']]>' ) |
| end |
| M.private.xmlCDataEscape = xmlCDataEscape |
| |
| local function stripLuaunitTrace( stackTrace ) |
| --[[ |
| -- Example of a traceback: |
| <<stack traceback: |
| example_with_luaunit.lua:130: in function 'test2_withFailure' |
| ./luaunit.lua:1449: in function <./luaunit.lua:1449> |
| [C]: in function 'xpcall' |
| ./luaunit.lua:1449: in function 'protectedCall' |
| ./luaunit.lua:1508: in function 'execOneFunction' |
| ./luaunit.lua:1596: in function 'runSuiteByInstances' |
| ./luaunit.lua:1660: in function 'runSuiteByNames' |
| ./luaunit.lua:1736: in function 'runSuite' |
| example_with_luaunit.lua:140: in main chunk |
| [C]: in ?>> |
| |
| Other example: |
| <<stack traceback: |
| ./luaunit.lua:545: in function 'assertEquals' |
| example_with_luaunit.lua:58: in function 'TestToto.test7' |
| ./luaunit.lua:1517: in function <./luaunit.lua:1517> |
| [C]: in function 'xpcall' |
| ./luaunit.lua:1517: in function 'protectedCall' |
| ./luaunit.lua:1578: in function 'execOneFunction' |
| ./luaunit.lua:1677: in function 'runSuiteByInstances' |
| ./luaunit.lua:1730: in function 'runSuiteByNames' |
| ./luaunit.lua:1806: in function 'runSuite' |
| example_with_luaunit.lua:140: in main chunk |
| [C]: in ?>> |
| |
| <<stack traceback: |
| luaunit2/example_with_luaunit.lua:124: in function 'test1_withFailure' |
| luaunit2/luaunit.lua:1532: in function <luaunit2/luaunit.lua:1532> |
| [C]: in function 'xpcall' |
| luaunit2/luaunit.lua:1532: in function 'protectedCall' |
| luaunit2/luaunit.lua:1591: in function 'execOneFunction' |
| luaunit2/luaunit.lua:1679: in function 'runSuiteByInstances' |
| luaunit2/luaunit.lua:1743: in function 'runSuiteByNames' |
| luaunit2/luaunit.lua:1819: in function 'runSuite' |
| luaunit2/example_with_luaunit.lua:140: in main chunk |
| [C]: in ?>> |
| |
| |
| -- first line is "stack traceback": KEEP |
| -- next line may be luaunit line: REMOVE |
| -- next lines are call in the program under testOk: REMOVE |
| -- next lines are calls from luaunit to call the program under test: KEEP |
| |
| -- Strategy: |
| -- keep first line |
| -- remove lines that are part of luaunit |
| -- kepp lines until we hit a luaunit line |
| ]] |
| |
| local function isLuaunitInternalLine( s ) |
| -- return true if line of stack trace comes from inside luaunit |
| return s:find('[/\\]luaunit%.lua:%d+: ') ~= nil |
| end |
| |
| -- print( '<<'..stackTrace..'>>' ) |
| |
| local t = strsplit( '\n', stackTrace ) |
| -- print( prettystr(t) ) |
| |
| local idx = 2 |
| |
| -- remove lines that are still part of luaunit |
| while t[idx] and isLuaunitInternalLine( t[idx] ) do |
| -- print('Removing : '..t[idx] ) |
| table.remove(t, idx) |
| end |
| |
| -- keep lines until we hit luaunit again |
| while t[idx] and (not isLuaunitInternalLine(t[idx])) do |
| -- print('Keeping : '..t[idx] ) |
| idx = idx + 1 |
| end |
| |
| -- remove remaining luaunit lines |
| while t[idx] do |
| -- print('Removing : '..t[idx] ) |
| table.remove(t, idx) |
| end |
| |
| -- print( prettystr(t) ) |
| return table.concat( t, '\n') |
| |
| end |
| M.private.stripLuaunitTrace = stripLuaunitTrace |
| |
| |
| local function prettystr_sub(v, indentLevel, keeponeline, printTableRefs, recursionTable ) |
| local type_v = type(v) |
| if "string" == type_v then |
| if keeponeline then v = v:gsub("\n", "\\n") end |
| |
| -- use clever delimiters according to content: |
| -- enclose with single quotes if string contains ", but no ' |
| if v:find('"', 1, true) and not v:find("'", 1, true) then |
| return "'" .. v .. "'" |
| end |
| -- use double quotes otherwise, escape embedded " |
| return '"' .. v:gsub('"', '\\"') .. '"' |
| |
| elseif "table" == type_v then |
| --if v.__class__ then |
| -- return string.gsub( tostring(v), 'table', v.__class__ ) |
| --end |
| return M.private._table_tostring(v, indentLevel, printTableRefs, recursionTable) |
| end |
| |
| return tostring(v) |
| end |
| |
| local function prettystr( v, keeponeline ) |
| --[[ Better string conversion, to display nice variable content: |
| For strings, if keeponeline is set to true, string is displayed on one line, with visible \n |
| * string are enclosed with " by default, or with ' if string contains a " |
| * if table is a class, display class name |
| * tables are expanded |
| ]]-- |
| local recursionTable = {} |
| local s = prettystr_sub(v, 1, keeponeline, M.PRINT_TABLE_REF_IN_ERROR_MSG, recursionTable) |
| if recursionTable.recursionDetected and not M.PRINT_TABLE_REF_IN_ERROR_MSG then |
| -- some table contain recursive references, |
| -- so we must recompute the value by including all table references |
| -- else the result looks like crap |
| recursionTable = {} |
| s = prettystr_sub(v, 1, keeponeline, true, recursionTable) |
| end |
| return s |
| end |
| M.prettystr = prettystr |
| |
| local function prettystrPadded(value1, value2, suffix_a, suffix_b) |
| --[[ |
| This function helps with the recurring task of constructing the "expected |
| vs. actual" error messages. It takes two arbitrary values and formats |
| corresponding strings with prettystr(). |
| |
| To keep the (possibly complex) output more readable in case the resulting |
| strings contain line breaks, they get automatically prefixed with additional |
| newlines. Both suffixes are optional (default to empty strings), and get |
| appended to the "value1" string. "suffix_a" is used if line breaks were |
| encountered, "suffix_b" otherwise. |
| |
| Returns the two formatted strings (including padding/newlines). |
| ]] |
| local str1, str2 = prettystr(value1), prettystr(value2) |
| if hasNewLine(str1) or hasNewLine(str2) then |
| -- line break(s) detected, add padding |
| return "\n" .. str1 .. (suffix_a or ""), "\n" .. str2 |
| end |
| return str1 .. (suffix_b or ""), str2 |
| end |
| M.private.prettystrPadded = prettystrPadded |
| |
| local function _table_keytostring(k) |
| -- like prettystr but do not enclose with "" if the string is just alphanumerical |
| -- this is better for displaying table keys who are often simple strings |
| if "string" == type(k) and k:match("^[_%a][_%w]*$") then |
| return k |
| end |
| return prettystr(k) |
| end |
| M.private._table_keytostring = _table_keytostring |
| |
| local TABLE_TOSTRING_SEP = ", " |
| local TABLE_TOSTRING_SEP_LEN = string.len(TABLE_TOSTRING_SEP) |
| |
| local function _table_tostring( tbl, indentLevel, printTableRefs, recursionTable ) |
| printTableRefs = printTableRefs or M.PRINT_TABLE_REF_IN_ERROR_MSG |
| recursionTable = recursionTable or {} |
| recursionTable[tbl] = true |
| |
| local result, dispOnMultLines = {}, false |
| |
| local entry, count, seq_index = nil, 0, 1 |
| for k, v in sortedPairs( tbl ) do |
| if k == seq_index then |
| -- for the sequential part of tables, we'll skip the "<key>=" output |
| entry = '' |
| seq_index = seq_index + 1 |
| else |
| entry = _table_keytostring( k ) .. "=" |
| end |
| if recursionTable[v] then -- recursion detected! |
| recursionTable.recursionDetected = true |
| entry = entry .. "<"..tostring(v)..">" |
| else |
| entry = entry .. |
| prettystr_sub( v, indentLevel+1, true, printTableRefs, recursionTable ) |
| end |
| count = count + 1 |
| result[count] = entry |
| end |
| |
| -- set dispOnMultLines if the maximum LINE_LENGTH would be exceeded |
| local totalLength = 0 |
| for k, v in ipairs( result ) do |
| totalLength = totalLength + string.len( v ) |
| if totalLength >= M.LINE_LENGTH then |
| dispOnMultLines = true |
| break |
| end |
| end |
| |
| if not dispOnMultLines then |
| -- adjust with length of separator(s): |
| -- two items need 1 sep, three items two seps, ... plus len of '{}' |
| if count > 0 then |
| totalLength = totalLength + TABLE_TOSTRING_SEP_LEN * (count - 1) |
| end |
| dispOnMultLines = totalLength + 2 >= M.LINE_LENGTH |
| end |
| |
| -- now reformat the result table (currently holding element strings) |
| if dispOnMultLines then |
| local indentString = string.rep(" ", indentLevel - 1) |
| result = {"{\n ", indentString, |
| table.concat(result, ",\n " .. indentString), "\n", |
| indentString, "}"} |
| else |
| result = {"{", table.concat(result, TABLE_TOSTRING_SEP), "}"} |
| end |
| if printTableRefs then |
| table.insert(result, 1, "<"..tostring(tbl).."> ") -- prepend table ref |
| end |
| return table.concat(result) |
| end |
| M.private._table_tostring = _table_tostring -- prettystr_sub() needs it |
| |
| local function _table_contains(t, element) |
| if t then |
| for _, value in pairs(t) do |
| if type(value) == type(element) then |
| if type(element) == 'table' then |
| -- if we wanted recursive items content comparison, we could use |
| -- _is_table_items_equals(v, expected) but one level of just comparing |
| -- items is sufficient |
| if M.private._is_table_equals( value, element ) then |
| return true |
| end |
| else |
| if value == element then |
| return true |
| end |
| end |
| end |
| end |
| end |
| return false |
| end |
| |
| local function _is_table_items_equals(actual, expected ) |
| if (type(actual) == 'table') and (type(expected) == 'table') then |
| for k,v in pairs(actual) do |
| if not _table_contains(expected, v) then |
| return false |
| end |
| end |
| for k,v in pairs(expected) do |
| if not _table_contains(actual, v) then |
| return false |
| end |
| end |
| return true |
| elseif type(actual) ~= type(expected) then |
| return false |
| elseif actual == expected then |
| return true |
| end |
| return false |
| end |
| |
| local function _is_table_equals(actual, expected) |
| if (type(actual) == 'table') and (type(expected) == 'table') then |
| if (#actual ~= #expected) then |
| return false |
| end |
| |
| local actualTableKeys = {} |
| for k,v in pairs(actual) do |
| if M.TABLE_EQUALS_KEYBYCONTENT and type(k) == "table" then |
| -- If the keys are tables, things get a bit tricky here as we |
| -- can have _is_table_equals(k1, k2) and t[k1] ~= t[k2]. So we |
| -- collect actual's table keys, group them by length for |
| -- performance, and then for each table key in expected we look |
| -- it up in actualTableKeys. |
| if not actualTableKeys[#k] then actualTableKeys[#k] = {} end |
| table.insert(actualTableKeys[#k], k) |
| else |
| if not _is_table_equals(v, expected[k]) then |
| return false |
| end |
| end |
| end |
| |
| for k,v in pairs(expected) do |
| if M.TABLE_EQUALS_KEYBYCONTENT and type(k) == "table" then |
| local candidates = actualTableKeys[#k] |
| if not candidates then return false end |
| local found |
| for i, candidate in pairs(candidates) do |
| if _is_table_equals(candidate, k) then |
| found = candidate |
| -- Remove the candidate we matched against from the list |
| -- of candidates, so each key in actual can only match |
| -- one key in expected. |
| candidates[i] = nil |
| break |
| end |
| end |
| if not(found and _is_table_equals(actual[found], v)) then return false end |
| else |
| if not _is_table_equals(v, actual[k]) then |
| return false |
| end |
| end |
| end |
| |
| if M.TABLE_EQUALS_KEYBYCONTENT then |
| for _, keys in pairs(actualTableKeys) do |
| -- if there are any keys left in any actualTableKeys[i] then |
| -- that is a key in actual with no matching key in expected, |
| -- and so the tables aren't equal. |
| if next(keys) then return false end |
| end |
| end |
| |
| return true |
| elseif type(actual) ~= type(expected) then |
| return false |
| elseif actual == expected then |
| return true |
| end |
| return false |
| end |
| M.private._is_table_equals = _is_table_equals |
| |
| local function failure(msg, level) |
| -- raise an error indicating a test failure |
| -- for error() compatibility we adjust "level" here (by +1), to report the |
| -- calling context |
| error(M.FAILURE_PREFIX .. msg, (level or 1) + 1) |
| end |
| |
| local function fail_fmt(level, ...) |
| -- failure with printf-style formatted message and given error level |
| failure(string.format(...), (level or 1) + 1) |
| end |
| M.private.fail_fmt = fail_fmt |
| |
| local function error_fmt(level, ...) |
| -- printf-style error() |
| error(string.format(...), (level or 1) + 1) |
| end |
| |
| ---------------------------------------------------------------- |
| -- |
| -- assertions |
| -- |
| ---------------------------------------------------------------- |
| |
| local function errorMsgEquality(actual, expected) |
| if not M.ORDER_ACTUAL_EXPECTED then |
| expected, actual = actual, expected |
| end |
| if type(expected) == 'string' or type(expected) == 'table' then |
| expected, actual = prettystrPadded(expected, actual) |
| return string.format("expected: %s\nactual: %s", expected, actual) |
| end |
| return string.format("expected: %s, actual: %s", |
| prettystr(expected), prettystr(actual)) |
| end |
| |
| function M.assertError(f, ...) |
| -- assert that calling f with the arguments will raise an error |
| -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error |
| if pcall( f, ... ) then |
| failure( "Expected an error when calling function but no error generated", 2 ) |
| end |
| end |
| |
| function M.assertTrue(value) |
| if not value then |
| failure("expected: true, actual: " ..prettystr(value), 2) |
| end |
| end |
| |
| function M.assertFalse(value) |
| if value then |
| failure("expected: false, actual: " ..prettystr(value), 2) |
| end |
| end |
| |
| function M.assertIsNil(value) |
| if value ~= nil then |
| failure("expected: nil, actual: " ..prettystr(value), 2) |
| end |
| end |
| |
| function M.assertNotIsNil(value) |
| if value == nil then |
| failure("expected non nil value, received nil", 2) |
| end |
| end |
| |
| function M.assertEquals(actual, expected) |
| if type(actual) == 'table' and type(expected) == 'table' then |
| if not _is_table_equals(actual, expected) then |
| failure( errorMsgEquality(actual, expected), 2 ) |
| end |
| elseif type(actual) ~= type(expected) then |
| failure( errorMsgEquality(actual, expected), 2 ) |
| elseif actual ~= expected then |
| failure( errorMsgEquality(actual, expected), 2 ) |
| end |
| end |
| |
| -- Help Lua in corner cases like almostEquals(1.1, 1.0, 0.1), which by default |
| -- may not work. We need to give margin a small boost; EPSILON defines the |
| -- default value to use for this: |
| local EPSILON = 0.00000000001 |
| function M.almostEquals( actual, expected, margin, margin_boost ) |
| if type(actual) ~= 'number' or type(expected) ~= 'number' or type(margin) ~= 'number' then |
| error_fmt(3, 'almostEquals: must supply only number arguments.\nArguments supplied: %s, %s, %s', |
| prettystr(actual), prettystr(expected), prettystr(margin)) |
| end |
| if margin <= 0 then |
| error('almostEquals: margin must be positive, current value is ' .. margin, 3) |
| end |
| local realmargin = margin + (margin_boost or EPSILON) |
| return math.abs(expected - actual) <= realmargin |
| end |
| |
| function M.assertAlmostEquals( actual, expected, margin ) |
| -- check that two floats are close by margin |
| if not M.almostEquals(actual, expected, margin) then |
| if not M.ORDER_ACTUAL_EXPECTED then |
| expected, actual = actual, expected |
| end |
| fail_fmt(2, 'Values are not almost equal\nExpected: %s with margin of %s, received: %s', |
| expected, margin, actual) |
| end |
| end |
| |
| function M.assertNotEquals(actual, expected) |
| if type(actual) ~= type(expected) then |
| return |
| end |
| |
| if type(actual) == 'table' and type(expected) == 'table' then |
| if not _is_table_equals(actual, expected) then |
| return |
| end |
| elseif actual ~= expected then |
| return |
| end |
| fail_fmt(2, 'Received the not expected value: %s', prettystr(actual)) |
| end |
| |
| function M.assertNotAlmostEquals( actual, expected, margin ) |
| -- check that two floats are not close by margin |
| if M.almostEquals(actual, expected, margin) then |
| if not M.ORDER_ACTUAL_EXPECTED then |
| expected, actual = actual, expected |
| end |
| fail_fmt(2, 'Values are almost equal\nExpected: %s with a difference above margin of %s, received: %s', |
| expected, margin, actual) |
| end |
| end |
| |
| function M.assertStrContains( str, sub, useRe ) |
| -- this relies on lua string.find function |
| -- a string always contains the empty string |
| if not string.find(str, sub, 1, not useRe) then |
| sub, str = prettystrPadded(sub, str, '\n') |
| fail_fmt(2, 'Error, %s %s was not found in string %s', |
| useRe and 'regexp' or 'substring', sub, str) |
| end |
| end |
| |
| function M.assertStrIContains( str, sub ) |
| -- this relies on lua string.find function |
| -- a string always contains the empty string |
| if not string.find(str:lower(), sub:lower(), 1, true) then |
| sub, str = prettystrPadded(sub, str, '\n') |
| fail_fmt(2, 'Error, substring %s was not found (case insensitively) in string %s', |
| sub, str) |
| end |
| end |
| |
| function M.assertNotStrContains( str, sub, useRe ) |
| -- this relies on lua string.find function |
| -- a string always contains the empty string |
| if string.find(str, sub, 1, not useRe) then |
| sub, str = prettystrPadded(sub, str, '\n') |
| fail_fmt(2, 'Error, %s %s was found in string %s', |
| useRe and 'regexp' or 'substring', sub, str) |
| end |
| end |
| |
| function M.assertNotStrIContains( str, sub ) |
| -- this relies on lua string.find function |
| -- a string always contains the empty string |
| if string.find(str:lower(), sub:lower(), 1, true) then |
| sub, str = prettystrPadded(sub, str, '\n') |
| fail_fmt(2, 'Error, substring %s was found (case insensitively) in string %s', |
| sub, str) |
| end |
| end |
| |
| function M.assertStrMatches( str, pattern, start, final ) |
| -- Verify a full match for the string |
| -- for a partial match, simply use assertStrContains with useRe set to true |
| if not strMatch( str, pattern, start, final ) then |
| pattern, str = prettystrPadded(pattern, str, '\n') |
| fail_fmt(2, 'Error, pattern %s was not matched by string %s', |
| pattern, str) |
| end |
| end |
| |
| function M.assertErrorMsgEquals( expectedMsg, func, ... ) |
| -- assert that calling f with the arguments will raise an error |
| -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error |
| local no_error, error_msg = pcall( func, ... ) |
| if no_error then |
| failure( 'No error generated when calling function but expected error: "'..expectedMsg..'"', 2 ) |
| end |
| if error_msg ~= expectedMsg then |
| error_msg, expectedMsg = prettystrPadded(error_msg, expectedMsg) |
| fail_fmt(2, 'Exact error message expected: %s\nError message received: %s\n', |
| expectedMsg, error_msg) |
| end |
| end |
| |
| function M.assertErrorMsgContains( partialMsg, func, ... ) |
| -- assert that calling f with the arguments will raise an error |
| -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error |
| local no_error, error_msg = pcall( func, ... ) |
| if no_error then |
| failure( 'No error generated when calling function but expected error containing: '..prettystr(partialMsg), 2 ) |
| end |
| if not string.find( error_msg, partialMsg, nil, true ) then |
| error_msg, partialMsg = prettystrPadded(error_msg, partialMsg) |
| fail_fmt(2, 'Error message does not contain: %s\nError message received: %s\n', |
| partialMsg, error_msg) |
| end |
| end |
| |
| function M.assertErrorMsgMatches( expectedMsg, func, ... ) |
| -- assert that calling f with the arguments will raise an error |
| -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error |
| local no_error, error_msg = pcall( func, ... ) |
| if no_error then |
| failure( 'No error generated when calling function but expected error matching: "'..expectedMsg..'"', 2 ) |
| end |
| if not strMatch( error_msg, expectedMsg ) then |
| expectedMsg, error_msg = prettystrPadded(expectedMsg, error_msg) |
| fail_fmt(2, 'Error message does not match: %s\nError message received: %s\n', |
| expectedMsg, error_msg) |
| end |
| end |
| |
| --[[ |
| Add type assertion functions to the module table M. Each of these functions |
| takes a single parameter "value", and checks that its Lua type matches the |
| expected string (derived from the function name): |
| |
| M.assertIsXxx(value) -> ensure that type(value) conforms to "xxx" |
| ]] |
| for _, funcName in ipairs( |
| {'assertIsNumber', 'assertIsString', 'assertIsTable', 'assertIsBoolean', |
| 'assertIsFunction', 'assertIsUserdata', 'assertIsThread'} |
| ) do |
| local typeExpected = funcName:match("^assertIs([A-Z]%a*)$") |
| -- Lua type() always returns lowercase, also make sure the match() succeeded |
| typeExpected = typeExpected and typeExpected:lower() |
| or error("bad function name '"..funcName.."' for type assertion") |
| |
| M[funcName] = function(value) |
| if type(value) ~= typeExpected then |
| fail_fmt(2, 'Expected: a %s value, actual: type %s, value %s', |
| typeExpected, type(value), prettystrPadded(value)) |
| end |
| end |
| end |
| |
| --[[ |
| Add non-type assertion functions to the module table M. Each of these functions |
| takes a single parameter "value", and checks that its Lua type differs from the |
| expected string (derived from the function name): |
| |
| M.assertNotIsXxx(value) -> ensure that type(value) is not "xxx" |
| ]] |
| for _, funcName in ipairs( |
| {'assertNotIsNumber', 'assertNotIsString', 'assertNotIsTable', 'assertNotIsBoolean', |
| 'assertNotIsFunction', 'assertNotIsUserdata', 'assertNotIsThread'} |
| ) do |
| local typeUnexpected = funcName:match("^assertNotIs([A-Z]%a*)$") |
| -- Lua type() always returns lowercase, also make sure the match() succeeded |
| typeUnexpected = typeUnexpected and typeUnexpected:lower() |
| or error("bad function name '"..funcName.."' for type assertion") |
| |
| M[funcName] = function(value) |
| if type(value) == typeUnexpected then |
| fail_fmt(2, 'Not expected: a %s type, actual: value %s', |
| typeUnexpected, prettystrPadded(value)) |
| end |
| end |
| end |
| |
| function M.assertIs(actual, expected) |
| if actual ~= expected then |
| if not M.ORDER_ACTUAL_EXPECTED then |
| actual, expected = expected, actual |
| end |
| expected, actual = prettystrPadded(expected, actual, '\n', ', ') |
| fail_fmt(2, 'Expected object and actual object are not the same\nExpected: %sactual: %s', |
| expected, actual) |
| end |
| end |
| |
| function M.assertNotIs(actual, expected) |
| if actual == expected then |
| if not M.ORDER_ACTUAL_EXPECTED then |
| expected = actual |
| end |
| fail_fmt(2, 'Expected object and actual object are the same object: %s', |
| prettystrPadded(expected)) |
| end |
| end |
| |
| function M.assertItemsEquals(actual, expected) |
| -- checks that the items of table expected |
| -- are contained in table actual. Warning, this function |
| -- is at least O(n^2) |
| if not _is_table_items_equals(actual, expected ) then |
| expected, actual = prettystrPadded(expected, actual) |
| fail_fmt(2, 'Contents of the tables are not identical:\nExpected: %s\nActual: %s', |
| expected, actual) |
| end |
| end |
| |
| ---------------------------------------------------------------- |
| -- Compatibility layer |
| ---------------------------------------------------------------- |
| |
| -- for compatibility with LuaUnit v2.x |
| function M.wrapFunctions(...) |
| io.stderr:write( [[Use of WrapFunction() is no longer needed. |
| Just prefix your test function names with "test" or "Test" and they |
| will be picked up and run by LuaUnit.]] ) |
| -- In LuaUnit version <= 2.1 , this function was necessary to include |
| -- a test function inside the global test suite. Nowadays, the functions |
| -- are simply run directly as part of the test discovery process. |
| -- so just do nothing ! |
| |
| --[[ |
| local testClass, testFunction |
| testClass = {} |
| local function storeAsMethod(idx, testName) |
| testFunction = _G[testName] |
| testClass[testName] = testFunction |
| end |
| for i,v in ipairs({...}) do |
| storeAsMethod( i, v ) |
| end |
| |
| return testClass |
| ]] |
| end |
| |
| local list_of_funcs = { |
| -- { official function name , alias } |
| |
| -- general assertions |
| { 'assertEquals' , 'assert_equals' }, |
| { 'assertItemsEquals' , 'assert_items_equals' }, |
| { 'assertNotEquals' , 'assert_not_equals' }, |
| { 'assertAlmostEquals' , 'assert_almost_equals' }, |
| { 'assertNotAlmostEquals' , 'assert_not_almost_equals' }, |
| { 'assertTrue' , 'assert_true' }, |
| { 'assertFalse' , 'assert_false' }, |
| { 'assertStrContains' , 'assert_str_contains' }, |
| { 'assertStrIContains' , 'assert_str_icontains' }, |
| { 'assertNotStrContains' , 'assert_not_str_contains' }, |
| { 'assertNotStrIContains' , 'assert_not_str_icontains' }, |
| { 'assertStrMatches' , 'assert_str_matches' }, |
| { 'assertError' , 'assert_error' }, |
| { 'assertErrorMsgEquals' , 'assert_error_msg_equals' }, |
| { 'assertErrorMsgContains' , 'assert_error_msg_contains' }, |
| { 'assertErrorMsgMatches' , 'assert_error_msg_matches' }, |
| { 'assertIs' , 'assert_is' }, |
| { 'assertNotIs' , 'assert_not_is' }, |
| { 'wrapFunctions' , 'WrapFunctions' }, |
| { 'wrapFunctions' , 'wrap_functions' }, |
| |
| -- type assertions: assertIsXXX -> assert_is_xxx |
| { 'assertIsNumber' , 'assert_is_number' }, |
| { 'assertIsString' , 'assert_is_string' }, |
| { 'assertIsTable' , 'assert_is_table' }, |
| { 'assertIsBoolean' , 'assert_is_boolean' }, |
| { 'assertIsNil' , 'assert_is_nil' }, |
| { 'assertIsFunction' , 'assert_is_function' }, |
| { 'assertIsThread' , 'assert_is_thread' }, |
| { 'assertIsUserdata' , 'assert_is_userdata' }, |
| |
| -- type assertions: assertIsXXX -> assertXxx |
| { 'assertIsNumber' , 'assertNumber' }, |
| { 'assertIsString' , 'assertString' }, |
| { 'assertIsTable' , 'assertTable' }, |
| { 'assertIsBoolean' , 'assertBoolean' }, |
| { 'assertIsNil' , 'assertNil' }, |
| { 'assertIsFunction' , 'assertFunction' }, |
| { 'assertIsThread' , 'assertThread' }, |
| { 'assertIsUserdata' , 'assertUserdata' }, |
| |
| -- type assertions: assertIsXXX -> assert_xxx (luaunit v2 compat) |
| { 'assertIsNumber' , 'assert_number' }, |
| { 'assertIsString' , 'assert_string' }, |
| { 'assertIsTable' , 'assert_table' }, |
| { 'assertIsBoolean' , 'assert_boolean' }, |
| { 'assertIsNil' , 'assert_nil' }, |
| { 'assertIsFunction' , 'assert_function' }, |
| { 'assertIsThread' , 'assert_thread' }, |
| { 'assertIsUserdata' , 'assert_userdata' }, |
| |
| -- type assertions: assertNotIsXXX -> assert_not_is_xxx |
| { 'assertNotIsNumber' , 'assert_not_is_number' }, |
| { 'assertNotIsString' , 'assert_not_is_string' }, |
| { 'assertNotIsTable' , 'assert_not_is_table' }, |
| { 'assertNotIsBoolean' , 'assert_not_is_boolean' }, |
| { 'assertNotIsNil' , 'assert_not_is_nil' }, |
| { 'assertNotIsFunction' , 'assert_not_is_function' }, |
| { 'assertNotIsThread' , 'assert_not_is_thread' }, |
| { 'assertNotIsUserdata' , 'assert_not_is_userdata' }, |
| |
| -- type assertions: assertNotIsXXX -> assertNotXxx (luaunit v2 compat) |
| { 'assertNotIsNumber' , 'assertNotNumber' }, |
| { 'assertNotIsString' , 'assertNotString' }, |
| { 'assertNotIsTable' , 'assertNotTable' }, |
| { 'assertNotIsBoolean' , 'assertNotBoolean' }, |
| { 'assertNotIsNil' , 'assertNotNil' }, |
| { 'assertNotIsFunction' , 'assertNotFunction' }, |
| { 'assertNotIsThread' , 'assertNotThread' }, |
| { 'assertNotIsUserdata' , 'assertNotUserdata' }, |
| |
| -- type assertions: assertNotIsXXX -> assert_not_xxx |
| { 'assertNotIsNumber' , 'assert_not_number' }, |
| { 'assertNotIsString' , 'assert_not_string' }, |
| { 'assertNotIsTable' , 'assert_not_table' }, |
| { 'assertNotIsBoolean' , 'assert_not_boolean' }, |
| { 'assertNotIsNil' , 'assert_not_nil' }, |
| { 'assertNotIsFunction' , 'assert_not_function' }, |
| { 'assertNotIsThread' , 'assert_not_thread' }, |
| { 'assertNotIsUserdata' , 'assert_not_userdata' }, |
| |
| -- all assertions with Coroutine duplicate Thread assertions |
| { 'assertIsThread' , 'assertIsCoroutine' }, |
| { 'assertIsThread' , 'assertCoroutine' }, |
| { 'assertIsThread' , 'assert_is_coroutine' }, |
| { 'assertIsThread' , 'assert_coroutine' }, |
| { 'assertNotIsThread' , 'assertNotIsCoroutine' }, |
| { 'assertNotIsThread' , 'assertNotCoroutine' }, |
| { 'assertNotIsThread' , 'assert_not_is_coroutine' }, |
| { 'assertNotIsThread' , 'assert_not_coroutine' }, |
| } |
| |
| -- Create all aliases in M |
| for _,v in ipairs( list_of_funcs ) do |
| funcname, alias = v[1], v[2] |
| M[alias] = M[funcname] |
| |
| if EXPORT_ASSERT_TO_GLOBALS then |
| _G[funcname] = M[funcname] |
| _G[alias] = M[funcname] |
| end |
| end |
| |
| ---------------------------------------------------------------- |
| -- |
| -- Outputters |
| -- |
| ---------------------------------------------------------------- |
| |
| ---------------------------------------------------------------- |
| -- class TapOutput |
| ---------------------------------------------------------------- |
| |
| |
| local TapOutput = { __class__ = 'TapOutput' } -- class |
| local TapOutput_MT = { __index = TapOutput } -- metatable |
| |
| -- For a good reference for TAP format, check: http://testanything.org/tap-specification.html |
| |
| function TapOutput:new() |
| return setmetatable( { verbosity = M.VERBOSITY_LOW }, TapOutput_MT) |
| end |
| function TapOutput:startSuite() |
| print("1.."..self.result.testCount) |
| print('# Started on '..self.result.startDate) |
| end |
| function TapOutput:startClass(className) |
| if className ~= '[TestFunctions]' then |
| print('# Starting class: '..className) |
| end |
| end |
| function TapOutput:startTest(testName) end |
| |
| function TapOutput:addFailure( node ) |
| io.stdout:write("not ok ", self.result.currentTestNumber, "\t", node.testName, "\n") |
| if self.verbosity > M.VERBOSITY_LOW then |
| print( prefixString( ' ', node.msg ) ) |
| end |
| if self.verbosity > M.VERBOSITY_DEFAULT then |
| print( prefixString( ' ', node.stackTrace ) ) |
| end |
| end |
| TapOutput.addError = TapOutput.addFailure |
| |
| function TapOutput:endTest( node ) |
| if node:isPassed() then |
| io.stdout:write("ok ", self.result.currentTestNumber, "\t", node.testName, "\n") |
| end |
| end |
| |
| function TapOutput:endClass() end |
| |
| function TapOutput:endSuite() |
| print( '# '..M.LuaUnit.statusLine( self.result ) ) |
| return self.result.notPassedCount |
| end |
| |
| |
| -- class TapOutput end |
| |
| ---------------------------------------------------------------- |
| -- class JUnitOutput |
| ---------------------------------------------------------------- |
| |
| -- See directory junitxml for more information about the junit format |
| local JUnitOutput = { __class__ = 'JUnitOutput' } -- class |
| local JUnitOutput_MT = { __index = JUnitOutput } -- metatable |
| |
| function JUnitOutput:new() |
| return setmetatable( |
| { testList = {}, verbosity = M.VERBOSITY_LOW }, JUnitOutput_MT) |
| end |
| function JUnitOutput:startSuite() |
| |
| -- open xml file early to deal with errors |
| if self.fname == nil then |
| error('With Junit, an output filename must be supplied with --name!') |
| end |
| if string.sub(self.fname,-4) ~= '.xml' then |
| self.fname = self.fname..'.xml' |
| end |
| self.fd = io.open(self.fname, "w") |
| if self.fd == nil then |
| error("Could not open file for writing: "..self.fname) |
| end |
| |
| print('# XML output to '..self.fname) |
| print('# Started on '..self.result.startDate) |
| end |
| function JUnitOutput:startClass(className) |
| if className ~= '[TestFunctions]' then |
| print('# Starting class: '..className) |
| end |
| end |
| function JUnitOutput:startTest(testName) |
| print('# Starting test: '..testName) |
| end |
| |
| function JUnitOutput:addFailure( node ) |
| print('# Failure: ' .. node.msg) |
| -- print('# ' .. node.stackTrace) |
| end |
| |
| function JUnitOutput:addError( node ) |
| print('# Error: ' .. node.msg) |
| -- print('# ' .. node.stackTrace) |
| end |
| |
| function JUnitOutput:endTest( node ) |
| end |
| |
| function JUnitOutput:endClass() |
| end |
| |
| function JUnitOutput:endSuite() |
| print( '# '..M.LuaUnit.statusLine(self.result)) |
| |
| -- XML file writing |
| self.fd:write('<?xml version="1.0" encoding="UTF-8" ?>\n') |
| self.fd:write('<testsuites>\n') |
| self.fd:write(string.format( |
| ' <testsuite name="LuaUnit" id="00001" package="" hostname="localhost" tests="%d" timestamp="%s" time="%0.3f" errors="%d" failures="%d">\n', |
| self.result.runCount, self.result.startIsodate, self.result.duration, self.result.errorCount, self.result.failureCount )) |
| self.fd:write(" <properties>\n") |
| self.fd:write(string.format(' <property name="Lua Version" value="%s"/>\n', _VERSION ) ) |
| self.fd:write(string.format(' <property name="LuaUnit Version" value="%s"/>\n', M.VERSION) ) |
| -- XXX please include system name and version if possible |
| self.fd:write(" </properties>\n") |
| |
| for i,node in ipairs(self.result.tests) do |
| self.fd:write(string.format(' <testcase classname="%s" name="%s" time="%0.3f">\n', |
| node.className, node.testName, node.duration ) ) |
| if node:isNotPassed() then |
| self.fd:write(node:statusXML()) |
| end |
| self.fd:write(' </testcase>\n') |
| end |
| |
| -- Next two lines are needed to validate junit ANT xsd, but really not useful in general: |
| self.fd:write(' <system-out/>\n') |
| self.fd:write(' <system-err/>\n') |
| |
| self.fd:write(' </testsuite>\n') |
| self.fd:write('</testsuites>\n') |
| self.fd:close() |
| return self.result.notPassedCount |
| end |
| |
| |
| -- class TapOutput end |
| |
| ---------------------------------------------------------------- |
| -- class TextOutput |
| ---------------------------------------------------------------- |
| |
| --[[ |
| |
| -- Python Non verbose: |
| |
| For each test: . or F or E |
| |
| If some failed tests: |
| ============== |
| ERROR / FAILURE: TestName (testfile.testclass) |
| --------- |
| Stack trace |
| |
| |
| then -------------- |
| then "Ran x tests in 0.000s" |
| then OK or FAILED (failures=1, error=1) |
| |
| -- Python Verbose: |
| testname (filename.classname) ... ok |
| testname (filename.classname) ... FAIL |
| testname (filename.classname) ... ERROR |
| |
| then -------------- |
| then "Ran x tests in 0.000s" |
| then OK or FAILED (failures=1, error=1) |
| |
| -- Ruby: |
| Started |
| . |
| Finished in 0.002695 seconds. |
| |
| 1 tests, 2 assertions, 0 failures, 0 errors |
| |
| -- Ruby: |
| >> ruby tc_simple_number2.rb |
| Loaded suite tc_simple_number2 |
| Started |
| F.. |
| Finished in 0.038617 seconds. |
| |
| 1) Failure: |
| test_failure(TestSimpleNumber) [tc_simple_number2.rb:16]: |
| Adding doesn't work. |
| <3> expected but was |
| <4>. |
| |
| 3 tests, 4 assertions, 1 failures, 0 errors |
| |
| -- Java Junit |
| .......F. |
| Time: 0,003 |
| There was 1 failure: |
| 1) testCapacity(junit.samples.VectorTest)junit.framework.AssertionFailedError |
| at junit.samples.VectorTest.testCapacity(VectorTest.java:87) |
| at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) |
| at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) |
| at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) |
| |
| FAILURES!!! |
| Tests run: 8, Failures: 1, Errors: 0 |
| |
| |
| -- Maven |
| |
| # mvn test |
| ------------------------------------------------------- |
| T E S T S |
| ------------------------------------------------------- |
| Running math.AdditionTest |
| Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: |
| 0.03 sec <<< FAILURE! |
| |
| Results : |
| |
| Failed tests: |
| testLireSymbole(math.AdditionTest) |
| |
| Tests run: 2, Failures: 1, Errors: 0, Skipped: 0 |
| |
| |
| -- LuaUnit |
| ---- non verbose |
| * display . or F or E when running tests |
| ---- verbose |
| * display test name + ok/fail |
| ---- |
| * blank line |
| * number) ERROR or FAILURE: TestName |
| Stack trace |
| * blank line |
| * number) ERROR or FAILURE: TestName |
| Stack trace |
| |
| then -------------- |
| then "Ran x tests in 0.000s (%d not selected, %d skipped)" |
| then OK or FAILED (failures=1, error=1) |
| |
| |
| ]] |
| |
| local TextOutput = { __class__ = 'TextOutput' } -- class |
| local TextOutput_MT = { __index = TextOutput } -- metatable |
| |
| function TextOutput:new() |
| return setmetatable( |
| { errorList = {}, verbosity = M.VERBOSITY_DEFAULT }, TextOutput_MT ) |
| end |
| |
| function TextOutput:startSuite() |
| if self.verbosity > M.VERBOSITY_DEFAULT then |
| print( 'Started on '.. self.result.startDate ) |
| end |
| end |
| |
| function TextOutput:startClass(className) |
| -- display nothing when starting a new class |
| end |
| |
| function TextOutput:startTest(testName) |
| if self.verbosity > M.VERBOSITY_DEFAULT then |
| io.stdout:write( " ", self.result.currentNode.testName, " ... " ) |
| end |
| end |
| |
| function TextOutput:addFailure( node ) |
| -- nothing |
| end |
| |
| function TextOutput:addError( node ) |
| -- nothing |
| end |
| |
| function TextOutput:endTest( node ) |
| if node:isPassed() then |
| if self.verbosity > M.VERBOSITY_DEFAULT then |
| io.stdout:write("Ok\n") |
| else |
| io.stdout:write(".") |
| end |
| else |
| if self.verbosity > M.VERBOSITY_DEFAULT then |
| print( node.status ) |
| print( node.msg ) |
| --[[ |
| -- find out when to do this: |
| if self.verbosity > M.VERBOSITY_DEFAULT then |
| print( node.stackTrace ) |
| end |
| ]] |
| else |
| -- write only the first character of status |
| io.stdout:write(string.sub(node.status, 1, 1)) |
| end |
| end |
| end |
| |
| function TextOutput:endClass() |
| -- nothing |
| end |
| |
| function TextOutput:displayOneFailedTest( index, failure ) |
| print(index..") "..failure.testName ) |
| print( failure.msg ) |
| print( failure.stackTrace ) |
| print() |
| end |
| |
| function TextOutput:displayFailedTests() |
| if self.result.notPassedCount == 0 then return end |
| print("Failed tests:") |
| print("-------------") |
| for i,v in ipairs(self.result.notPassed) do |
| self:displayOneFailedTest( i, v ) |
| end |
| end |
| |
| function TextOutput:endSuite() |
| if self.verbosity > M.VERBOSITY_DEFAULT then |
| print("=========================================================") |
| else |
| print() |
| end |
| self:displayFailedTests() |
| print( M.LuaUnit.statusLine( self.result ) ) |
| local ignoredString = "" |
| if self.result.notPassedCount == 0 then |
| print('OK') |
| end |
| end |
| |
| -- class TextOutput end |
| |
| |
| ---------------------------------------------------------------- |
| -- class NilOutput |
| ---------------------------------------------------------------- |
| |
| local function nopCallable() |
| --print(42) |
| return nopCallable |
| end |
| |
| local NilOutput = { __class__ = 'NilOuptut' } -- class |
| local NilOutput_MT = { __index = nopCallable } -- metatable |
| |
| function NilOutput:new() |
| return setmetatable( { __class__ = 'NilOutput' }, NilOutput_MT ) |
| end |
| |
| ---------------------------------------------------------------- |
| -- |
| -- class LuaUnit |
| -- |
| ---------------------------------------------------------------- |
| |
| M.LuaUnit = { |
| outputType = TextOutput, |
| verbosity = M.VERBOSITY_DEFAULT, |
| __class__ = 'LuaUnit' |
| } |
| local LuaUnit_MT = { __index = M.LuaUnit } |
| |
| if EXPORT_ASSERT_TO_GLOBALS then |
| LuaUnit = M.LuaUnit |
| end |
| |
| function M.LuaUnit:new() |
| return setmetatable( {}, LuaUnit_MT ) |
| end |
| |
| -----------------[[ Utility methods ]]--------------------- |
| |
| function M.LuaUnit.asFunction(aObject) |
| -- return "aObject" if it is a function, and nil otherwise |
| if 'function' == type(aObject) then return aObject end |
| end |
| |
| function M.LuaUnit.isClassMethod(aName) |
| -- return true if aName contains a class + a method name in the form class:method |
| return string.find(aName, '.', nil, true) ~= nil |
| end |
| |
| function M.LuaUnit.splitClassMethod(someName) |
| -- return a pair className, methodName for a name in the form class:method |
| -- return nil if not a class + method name |
| -- name is class + method |
| local hasMethod, methodName, className |
| hasMethod = string.find(someName, '.', nil, true ) |
| if not hasMethod then return nil end |
| methodName = string.sub(someName, hasMethod+1) |
| className = string.sub(someName,1,hasMethod-1) |
| return className, methodName |
| end |
| |
| function M.LuaUnit.isMethodTestName( s ) |
| -- return true is the name matches the name of a test method |
| -- default rule is that is starts with 'Test' or with 'test' |
| return string.sub(s, 1, 4):lower() == 'test' |
| end |
| |
| function M.LuaUnit.isTestName( s ) |
| -- return true is the name matches the name of a test |
| -- default rule is that is starts with 'Test' or with 'test' |
| return string.sub(s, 1, 4):lower() == 'test' |
| end |
| |
| function M.LuaUnit.collectTests() |
| -- return a list of all test names in the global namespace |
| -- that match LuaUnit.isTestName |
| |
| local testNames = {} |
| for k, v in pairs(_G) do |
| if M.LuaUnit.isTestName( k ) then |
| table.insert( testNames , k ) |
| end |
| end |
| table.sort( testNames ) |
| return testNames |
| end |
| |
| function M.LuaUnit.parseCmdLine( cmdLine ) |
| -- parse the command line |
| -- Supported command line parameters: |
| -- --verbose, -v: increase verbosity |
| -- --quiet, -q: silence output |
| -- --error, -e: treat errors as fatal (quit program) |
| -- --output, -o, + name: select output type |
| -- --pattern, -p, + pattern: run test matching pattern, may be repeated |
| -- --name, -n, + fname: name of output file for junit, default to stdout |
| -- [testnames, ...]: run selected test names |
| -- |
| -- Returns a table with the following fields: |
| -- verbosity: nil, M.VERBOSITY_DEFAULT, M.VERBOSITY_QUIET, M.VERBOSITY_VERBOSE |
| -- output: nil, 'tap', 'junit', 'text', 'nil' |
| -- testNames: nil or a list of test names to run |
| -- pattern: nil or a list of patterns |
| |
| local result = {} |
| local state = nil |
| local SET_OUTPUT = 1 |
| local SET_PATTERN = 2 |
| local SET_FNAME = 3 |
| |
| if cmdLine == nil then |
| return result |
| end |
| |
| local function parseOption( option ) |
| if option == '--help' or option == '-h' then |
| result['help'] = true |
| return |
| elseif option == '--version' then |
| result['version'] = true |
| return |
| elseif option == '--verbose' or option == '-v' then |
| result['verbosity'] = M.VERBOSITY_VERBOSE |
| return |
| elseif option == '--quiet' or option == '-q' then |
| result['verbosity'] = M.VERBOSITY_QUIET |
| return |
| elseif option == '--error' or option == '-e' then |
| result['quitOnError'] = true |
| return |
| elseif option == '--failure' or option == '-f' then |
| result['quitOnFailure'] = true |
| return |
| elseif option == '--output' or option == '-o' then |
| state = SET_OUTPUT |
| return state |
| elseif option == '--name' or option == '-n' then |
| state = SET_FNAME |
| return state |
| elseif option == '--pattern' or option == '-p' then |
| state = SET_PATTERN |
| return state |
| end |
| error('Unknown option: '..option,3) |
| end |
| |
| local function setArg( cmdArg, state ) |
| if state == SET_OUTPUT then |
| result['output'] = cmdArg |
| return |
| elseif state == SET_FNAME then |
| result['fname'] = cmdArg |
| return |
| elseif state == SET_PATTERN then |
| if result['pattern'] then |
| table.insert( result['pattern'], cmdArg ) |
| else |
| result['pattern'] = { cmdArg } |
| end |
| return |
| end |
| error('Unknown parse state: '.. state) |
| end |
| |
| |
| for i, cmdArg in ipairs(cmdLine) do |
| if state ~= nil then |
| setArg( cmdArg, state, result ) |
| state = nil |
| else |
| if cmdArg:sub(1,1) == '-' then |
| state = parseOption( cmdArg ) |
| else |
| if result['testNames'] then |
| table.insert( result['testNames'], cmdArg ) |
| else |
| result['testNames'] = { cmdArg } |
| end |
| end |
| end |
| end |
| |
| if result['help'] then |
| M.LuaUnit.help() |
| end |
| |
| if result['version'] then |
| M.LuaUnit.version() |
| end |
| |
| if state ~= nil then |
| error('Missing argument after '..cmdLine[ #cmdLine ],2 ) |
| end |
| |
| return result |
| end |
| |
| function M.LuaUnit.help() |
| print(M.USAGE) |
| os.exit(0) |
| end |
| |
| function M.LuaUnit.version() |
| print('LuaUnit v'..M.VERSION..' by Philippe Fremy <phil@freehackers.org>') |
| os.exit(0) |
| end |
| |
| function M.LuaUnit.patternInclude( patternFilter, expr ) |
| -- check if any of patternFilter is contained in expr. If so, return true. |
| -- return false if None of the patterns are contained in expr |
| -- if patternFilter is nil, return true (no filtering) |
| if patternFilter == nil then |
| return true |
| end |
| |
| for i,pattern in ipairs(patternFilter) do |
| if string.find(expr, pattern) then |
| return true |
| end |
| end |
| |
| return false |
| end |
| |
| ---------------------------------------------------------------- |
| -- class NodeStatus |
| ---------------------------------------------------------------- |
| |
| local NodeStatus = { __class__ = 'NodeStatus' } -- class |
| local NodeStatus_MT = { __index = NodeStatus } -- metatable |
| M.NodeStatus = NodeStatus |
| |
| -- values of status |
| NodeStatus.PASS = 'PASS' |
| NodeStatus.FAIL = 'FAIL' |
| NodeStatus.ERROR = 'ERROR' |
| |
| function NodeStatus:new( number, testName, className ) |
| local t = { number = number, testName = testName, className = className } |
| setmetatable( t, NodeStatus_MT ) |
| t:pass() |
| return t |
| end |
| |
| function NodeStatus:pass() |
| self.status = self.PASS |
| -- useless but we know it's the field we want to use |
| self.msg = nil |
| self.stackTrace = nil |
| end |
| |
| function NodeStatus:fail(msg, stackTrace) |
| self.status = self.FAIL |
| self.msg = msg |
| self.stackTrace = stackTrace |
| end |
| |
| function NodeStatus:error(msg, stackTrace) |
| self.status = self.ERROR |
| self.msg = msg |
| self.stackTrace = stackTrace |
| end |
| |
| function NodeStatus:isPassed() |
| return self.status == NodeStatus.PASS |
| end |
| |
| function NodeStatus:isNotPassed() |
| -- print('hasFailure: '..prettystr(self)) |
| return self.status ~= NodeStatus.PASS |
| end |
| |
| function NodeStatus:isFailure() |
| return self.status == NodeStatus.FAIL |
| end |
| |
| function NodeStatus:isError() |
| return self.status == NodeStatus.ERROR |
| end |
| |
| function NodeStatus:statusXML() |
| if self:isError() then |
| return table.concat( |
| {' <error type="', xmlEscape(self.msg), '">\n', |
| ' <![CDATA[', xmlCDataEscape(self.stackTrace), |
| ']]></error>\n'}) |
| elseif self:isFailure() then |
| return table.concat( |
| {' <failure type="', xmlEscape(self.msg), '">\n', |
| ' <![CDATA[', xmlCDataEscape(self.stackTrace), |
| ']]></failure>\n'}) |
| end |
| return ' <passed/>\n' -- (not XSD-compliant! normally shouldn't get here) |
| end |
| |
| --------------[[ Output methods ]]------------------------- |
| |
| function M.LuaUnit.statusLine(result) |
| -- return status line string according to results |
| local s = string.format('Ran %d tests in %0.3f seconds, %d successes', |
| result.runCount, result.duration, result.passedCount ) |
| if result.notPassedCount > 0 then |
| if result.failureCount > 0 then |
| s = s..string.format(', %d failures', result.failureCount ) |
| end |
| if result.errorCount > 0 then |
| s = s..string.format(', %d errors', result.errorCount ) |
| end |
| else |
| s = s..', 0 failures' |
| end |
| if result.nonSelectedCount > 0 then |
| s = s..string.format(", %d non-selected", result.nonSelectedCount ) |
| end |
| return s |
| end |
| |
| function M.LuaUnit:startSuite(testCount, nonSelectedCount) |
| self.result = {} |
| self.result.testCount = testCount |
| self.result.nonSelectedCount = nonSelectedCount |
| self.result.passedCount = 0 |
| self.result.runCount = 0 |
| self.result.currentTestNumber = 0 |
| self.result.currentClassName = "" |
| self.result.currentNode = nil |
| self.result.suiteStarted = true |
| self.result.startTime = os.clock() |
| self.result.startDate = os.date(os.getenv('LUAUNIT_DATEFMT')) |
| self.result.startIsodate = os.date('%Y-%m-%dT%H:%M:%S') |
| self.result.patternFilter = self.patternFilter |
| self.result.tests = {} |
| self.result.failures = {} |
| self.result.errors = {} |
| self.result.notPassed = {} |
| |
| self.outputType = self.outputType or TextOutput |
| self.output = self.outputType:new() |
| self.output.runner = self |
| self.output.result = self.result |
| self.output.verbosity = self.verbosity |
| self.output.fname = self.fname |
| self.output:startSuite() |
| end |
| |
| function M.LuaUnit:startClass( className ) |
| self.result.currentClassName = className |
| self.output:startClass( className ) |
| end |
| |
| function M.LuaUnit:startTest( testName ) |
| self.result.currentTestNumber = self.result.currentTestNumber + 1 |
| self.result.runCount = self.result.runCount + 1 |
| self.result.currentNode = NodeStatus:new( |
| self.result.currentTestNumber, |
| testName, |
| self.result.currentClassName |
| ) |
| self.result.currentNode.startTime = os.clock() |
| table.insert( self.result.tests, self.result.currentNode ) |
| self.output:startTest( testName ) |
| end |
| |
| function M.LuaUnit:addStatus( err ) |
| -- "err" is expected to be a table / result from protectedCall() |
| if err.status == NodeStatus.PASS then return end |
| |
| local node = self.result.currentNode |
| |
| --[[ As a first approach, we will report only one error or one failure for one test. |
| |
| However, we can have the case where the test is in failure, and the teardown is in error. |
| In such case, it's a good idea to report both a failure and an error in the test suite. This is |
| what Python unittest does for example. However, it mixes up counts so need to be handled carefully: for |
| example, there could be more (failures + errors) count that tests. What happens to the current node ? |
| |
| We will do this more intelligent version later. |
| ]] |
| |
| -- if the node is already in failure/error, just don't report the new error (see above) |
| if node.status ~= NodeStatus.PASS then return end |
| |
| table.insert( self.result.notPassed, node ) |
| |
| if err.status == NodeStatus.FAIL then |
| node:fail( err.msg, err.trace ) |
| table.insert( self.result.failures, node ) |
| self.output:addFailure( node ) |
| elseif err.status == NodeStatus.ERROR then |
| node:error( err.msg, err.trace ) |
| table.insert( self.result.errors, node ) |
| self.output:addError( node ) |
| end |
| end |
| |
| function M.LuaUnit:endTest() |
| local node = self.result.currentNode |
| -- print( 'endTest() '..prettystr(node)) |
| -- print( 'endTest() '..prettystr(node:isNotPassed())) |
| node.duration = os.clock() - node.startTime |
| node.startTime = nil |
| self.output:endTest( node ) |
| |
| if node:isPassed() then |
| self.result.passedCount = self.result.passedCount + 1 |
| elseif node:isError() then |
| if self.quitOnError or self.quitOnFailure then |
| -- Runtime error - abort test execution as requested by |
| -- "--error" option. This is done by setting a special |
| -- flag that gets handled in runSuiteByInstances(). |
| print("\nERROR during LuaUnit test execution:\n" .. node.msg) |
| self.result.aborted = true |
| end |
| elseif node:isFailure() then |
| if self.quitOnFailure then |
| -- Failure - abort test execution as requested by |
| -- "--failure" option. This is done by setting a special |
| -- flag that gets handled in runSuiteByInstances(). |
| print("\nFailure during LuaUnit test execution:\n" .. node.msg) |
| self.result.aborted = true |
| end |
| end |
| self.result.currentNode = nil |
| end |
| |
| function M.LuaUnit:endClass() |
| self.output:endClass() |
| end |
| |
| function M.LuaUnit:endSuite() |
| if self.result.suiteStarted == false then |
| error('LuaUnit:endSuite() -- suite was already ended' ) |
| end |
| self.result.duration = os.clock()-self.result.startTime |
| self.result.suiteStarted = false |
| |
| -- Expose test counts for outputter's endSuite(). This could be managed |
| -- internally instead, but unit tests (and existing use cases) might |
| -- rely on these fields being present. |
| self.result.notPassedCount = #self.result.notPassed |
| self.result.failureCount = #self.result.failures |
| self.result.errorCount = #self.result.errors |
| |
| self.output:endSuite() |
| end |
| |
| function M.LuaUnit:setOutputType(outputType) |
| -- default to text |
| -- tap produces results according to TAP format |
| if outputType:upper() == "NIL" then |
| self.outputType = NilOutput |
| return |
| end |
| if outputType:upper() == "TAP" then |
| self.outputType = TapOutput |
| return |
| end |
| if outputType:upper() == "JUNIT" then |
| self.outputType = JUnitOutput |
| return |
| end |
| if outputType:upper() == "TEXT" then |
| self.outputType = TextOutput |
| return |
| end |
| error( 'No such format: '..outputType,2) |
| end |
| |
| --------------[[ Runner ]]----------------- |
| |
| function M.LuaUnit:protectedCall(classInstance, methodInstance, prettyFuncName) |
| -- if classInstance is nil, this is just a function call |
| -- else, it's method of a class being called. |
| |
| local function err_handler(e) |
| -- transform error into a table, adding the traceback information |
| return { |
| status = NodeStatus.ERROR, |
| msg = e, |
| trace = string.sub(debug.traceback("", 3), 2) |
| } |
| end |
| |
| local ok, err |
| if classInstance then |
| -- stupid Lua < 5.2 does not allow xpcall with arguments so let's use a workaround |
| ok, err = xpcall( function () methodInstance(classInstance) end, err_handler ) |
| else |
| ok, err = xpcall( function () methodInstance() end, err_handler ) |
| end |
| if ok then |
| return {status = NodeStatus.PASS} |
| end |
| |
| -- determine if the error was a failed test: |
| -- We do this by stripping the failure prefix from the error message, |
| -- while keeping track of the gsub() count. A non-zero value -> failure |
| local failed |
| err.msg, failed = err.msg:gsub(M.FAILURE_PREFIX, "", 1) |
| if failed > 0 then |
| err.status = NodeStatus.FAIL |
| end |
| |
| -- reformat / improve the stack trace |
| if prettyFuncName then -- we do have the real method name |
| err.trace = err.trace:gsub("in (%a+) 'methodInstance'", "in %1 '"..prettyFuncName.."'") |
| end |
| if STRIP_LUAUNIT_FROM_STACKTRACE then |
| err.trace = stripLuaunitTrace(err.trace) |
| end |
| |
| return err -- return the error "object" (table) |
| end |
| |
| |
| function M.LuaUnit:execOneFunction(className, methodName, classInstance, methodInstance) |
| -- When executing a test function, className and classInstance must be nil |
| -- When executing a class method, all parameters must be set |
| |
| if type(methodInstance) ~= 'function' then |
| error( tostring(methodName)..' must be a function, not '..type(methodInstance)) |
| end |
| |
| local prettyFuncName |
| if className == nil then |
| className = '[TestFunctions]' |
| prettyFuncName = methodName |
| else |
| prettyFuncName = className..'.'..methodName |
| end |
| |
| if self.lastClassName ~= className then |
| if self.lastClassName ~= nil then |
| self:endClass() |
| end |
| self:startClass( className ) |
| self.lastClassName = className |
| end |
| |
| self:startTest(prettyFuncName) |
| |
| -- run setUp first (if any) |
| if classInstance then |
| local func = self.asFunction( classInstance.setUp ) |
| or self.asFunction( classInstance.Setup ) |
| or self.asFunction( classInstance.setup ) |
| or self.asFunction( classInstance.SetUp ) |
| if func then |
| self:addStatus(self:protectedCall(classInstance, func, className..'.setUp')) |
| end |
| end |
| |
| -- run testMethod() |
| if self.result.currentNode:isPassed() then |
| self:addStatus(self:protectedCall(classInstance, methodInstance, prettyFuncName)) |
| end |
| |
| -- lastly, run tearDown (if any) |
| if classInstance then |
| local func = self.asFunction( classInstance.tearDown ) |
| or self.asFunction( classInstance.TearDown ) |
| or self.asFunction( classInstance.teardown ) |
| or self.asFunction( classInstance.Teardown ) |
| if func then |
| self:addStatus(self:protectedCall(classInstance, func, className..'.tearDown')) |
| end |
| end |
| |
| self:endTest() |
| end |
| |
| function M.LuaUnit.expandOneClass( result, className, classInstance ) |
| -- add all test methods of classInstance to result |
| for methodName, methodInstance in sortedPairs(classInstance) do |
| if M.LuaUnit.asFunction(methodInstance) and M.LuaUnit.isMethodTestName( methodName ) then |
| table.insert( result, { className..'.'..methodName, classInstance } ) |
| end |
| end |
| end |
| |
| function M.LuaUnit.expandClasses( listOfNameAndInst ) |
| -- expand all classes (provided as {className, classInstance}) to a list of {className.methodName, classInstance} |
| -- functions and methods remain untouched |
| local result = {} |
| |
| for i,v in ipairs( listOfNameAndInst ) do |
| local name, instance = v[1], v[2] |
| if M.LuaUnit.asFunction(instance) then |
| table.insert( result, { name, instance } ) |
| else |
| if type(instance) ~= 'table' then |
| error( 'Instance must be a table or a function, not a '..type(instance)..', value '..prettystr(instance)) |
| end |
| if M.LuaUnit.isClassMethod( name ) then |
| local className, methodName = M.LuaUnit.splitClassMethod( name ) |
| local methodInstance = instance[methodName] |
| if methodInstance == nil then |
| error( "Could not find method in class "..tostring(className).." for method "..tostring(methodName) ) |
| end |
| table.insert( result, { name, instance } ) |
| else |
| M.LuaUnit.expandOneClass( result, name, instance ) |
| end |
| end |
| end |
| |
| return result |
| end |
| |
| function M.LuaUnit.applyPatternFilter( patternFilter, listOfNameAndInst ) |
| local included, excluded = {}, {} |
| for i, v in ipairs( listOfNameAndInst ) do |
| -- local name, instance = v[1], v[2] |
| if M.LuaUnit.patternInclude( patternFilter, v[1] ) then |
| table.insert( included, v ) |
| else |
| table.insert( excluded, v ) |
| end |
| end |
| return included, excluded |
| end |
| |
| function M.LuaUnit:runSuiteByInstances( listOfNameAndInst ) |
| -- Run an explicit list of tests. All test instances and names must be supplied. |
| -- each test must be one of: |
| -- * { function name, function instance } |
| -- * { class name, class instance } |
| -- * { class.method name, class instance } |
| |
| local expandedList, filteredList, filteredOutList, className, methodName, methodInstance |
| expandedList = self.expandClasses( listOfNameAndInst ) |
| |
| filteredList, filteredOutList = self.applyPatternFilter( self.patternFilter, expandedList ) |
| |
| self:startSuite( #filteredList, #filteredOutList ) |
| |
| for i,v in ipairs( filteredList ) do |
| local name, instance = v[1], v[2] |
| if M.LuaUnit.asFunction(instance) then |
| self:execOneFunction( nil, name, nil, instance ) |
| else |
| if type(instance) ~= 'table' then |
| error( 'Instance must be a table or a function, not a '..type(instance)..', value '..prettystr(instance)) |
| else |
| assert( M.LuaUnit.isClassMethod( name ) ) |
| className, methodName = M.LuaUnit.splitClassMethod( name ) |
| methodInstance = instance[methodName] |
| if methodInstance == nil then |
| error( "Could not find method in class "..tostring(className).." for method "..tostring(methodName) ) |
| end |
| self:execOneFunction( className, methodName, instance, methodInstance ) |
| end |
| end |
| if self.result.aborted then break end -- "--error" or "--failure" option triggered |
| end |
| |
| if self.lastClassName ~= nil then |
| self:endClass() |
| end |
| |
| self:endSuite() |
| |
| if self.result.aborted then |
| print("LuaUnit ABORTED (as requested by --error or --failure option)") |
| os.exit(-2) |
| end |
| end |
| |
| function M.LuaUnit:runSuiteByNames( listOfName ) |
| -- Run an explicit list of test names |
| |
| local className, methodName, instanceName, instance, methodInstance |
| local listOfNameAndInst = {} |
| |
| for i,name in ipairs( listOfName ) do |
| if M.LuaUnit.isClassMethod( name ) then |
| className, methodName = M.LuaUnit.splitClassMethod( name ) |
| instanceName = className |
| instance = _G[instanceName] |
| |
| if instance == nil then |
| error( "No such name in global space: "..instanceName ) |
| end |
| |
| if type(instance) ~= 'table' then |
| error( 'Instance of '..instanceName..' must be a table, not '..type(instance)) |
| end |
| |
| methodInstance = instance[methodName] |
| if methodInstance == nil then |
| error( "Could not find method in class "..tostring(className).." for method "..tostring(methodName) ) |
| end |
| |
| else |
| -- for functions and classes |
| instanceName = name |
| instance = _G[instanceName] |
| end |
| |
| if instance == nil then |
| error( "No such name in global space: "..instanceName ) |
| end |
| |
| if (type(instance) ~= 'table' and type(instance) ~= 'function') then |
| error( 'Name must match a function or a table: '..instanceName ) |
| end |
| |
| table.insert( listOfNameAndInst, { name, instance } ) |
| end |
| |
| self:runSuiteByInstances( listOfNameAndInst ) |
| end |
| |
| function M.LuaUnit.run(...) |
| -- Run some specific test classes. |
| -- If no arguments are passed, run the class names specified on the |
| -- command line. If no class name is specified on the command line |
| -- run all classes whose name starts with 'Test' |
| -- |
| -- If arguments are passed, they must be strings of the class names |
| -- that you want to run or generic command line arguments (-o, -p, -v, ...) |
| |
| local runner = M.LuaUnit.new() |
| return runner:runSuite(...) |
| end |
| |
| function M.LuaUnit:runSuite( ... ) |
| |
| local args = {...} |
| if type(args[1]) == 'table' and args[1].__class__ == 'LuaUnit' then |
| -- run was called with the syntax M.LuaUnit:runSuite() |
| -- we support both M.LuaUnit.run() and M.LuaUnit:run() |
| -- strip out the first argument |
| table.remove(args,1) |
| end |
| |
| if #args == 0 then |
| args = cmdline_argv |
| end |
| |
| local no_error, val = pcall( M.LuaUnit.parseCmdLine, args ) |
| if not no_error then |
| print(val) -- error message |
| print() |
| print(M.USAGE) |
| os.exit(-1) |
| end |
| |
| local options = val |
| |
| -- We expect these option fields to be either `nil` or contain |
| -- valid values, so it's safe to always copy them directly. |
| self.verbosity = options.verbosity |
| self.quitOnError = options.quitOnError |
| self.quitOnFailure = options.quitOnFailure |
| self.fname = options.fname |
| self.patternFilter = options.pattern |
| |
| if options.output and options.output:lower() == 'junit' and options.fname == nil then |
| print('With junit output, a filename must be supplied with -n or --name') |
| os.exit(-1) |
| end |
| |
| if options.output then |
| no_error, val = pcall(self.setOutputType, self, options.output) |
| if not no_error then |
| print(val) -- error message |
| print() |
| print(M.USAGE) |
| os.exit(-1) |
| end |
| end |
| |
| self:runSuiteByNames( options.testNames or M.LuaUnit.collectTests() ) |
| |
| return self.result.notPassedCount |
| end |
| -- class LuaUnit |
| |
| -- For compatibility with LuaUnit v2 |
| M.run = M.LuaUnit.run |
| M.Run = M.LuaUnit.run |
| |
| function M:setVerbosity( verbosity ) |
| M.LuaUnit.verbosity = verbosity |
| end |
| M.set_verbosity = M.setVerbosity |
| M.SetVerbosity = M.setVerbosity |
| |
| |
| return M |