--------------------------------------------------------------------------------- Unit tests for Scribunto.-------------------------------------------------------------------------------require('strict')
local DebugHelper = local ScribuntoUnit =
-- The cfg table contains all localisable strings and configuration, to make it-- easier to port this module to another wiki.local cfg = mw.loadData('Module:ScribuntoUnit/config')
--------------------------------------------------------------------------------- Concatenates keys and values, ideal for displaying a template or parser function argument table.-- @param keySeparator glue between key and value (defaults to " = ")-- @param separator glue between different key-value pairs (defaults to ", ")-- @example concatWithKeys(' => ', ', ') => "a => 1, b => 2, c => 3"-- function DebugHelper.concatWithKeys(table, keySeparator, separator) keySeparator = keySeparator or ' = ' separator = separator or ', ' local concatted = local i = 1 local first = true local unnamedArguments = true for k, v in pairs(table) do if first then first = false else concatted = concatted .. separator end if k
--------------------------------------------------------------------------------- Compares two tables recursively (non-table values are handled correctly as well).-- @param ignoreMetatable if false, t1.__eq is used for the comparison-- function DebugHelper.deepCompare(t1, t2, ignoreMetatable) local type1 = type(t1) local type2 = type(t2)
if type1 ~= type2 then return false end if type1 ~= 'table' then return t1
local metatable = getmetatable(t1) if not ignoreMetatable and metatable and metatable.__eq then return t1
for k1, v1 in pairs(t1) do local v2 = t2[k1] if v2
nil then return false end end
return trueend
--------------------------------------------------------------------------------- Raises an error with stack information-- @param details a table with error details-- - should have a 'text' key which is the error message to display-- - a 'trace' key will be added with the stack data-- - and a 'source' key with file/line number-- - a metatable will be added for error handling-- function DebugHelper.raise(details, level) level = (level or 1) + 1 details.trace = debug.traceback(, level) details.source = string.match(details.trace, '^%s*stack traceback:%s*(%S*:)')
-- setmetatable(details,)
error(details, level)end
--------------------------------------------------------------------------------- when used in a test, that test gets ignored, and the skipped count increases by one.-- function ScribuntoUnit:markTestSkipped DebugHelper.raise(3)end
--------------------------------------------------------------------------------- Checks that the input is true-- @param message optional description of the test-- function ScribuntoUnit:assertTrue(actual, message) if not actual then DebugHelper.raise(2) endend
--------------------------------------------------------------------------------- Checks that the input is false-- @param message optional description of the test-- function ScribuntoUnit:assertFalse(actual, message) if actual then DebugHelper.raise(2) endend
--------------------------------------------------------------------------------- Checks an input string contains the expected string-- @param message optional description of the test-- @param plain search is made with a plain string instead of a ustring pattern-- function ScribuntoUnit:assertStringContains(pattern, s, plain, message) if type(pattern) ~= 'string' then DebugHelper.raise(2) end if type(s) ~= 'string' then DebugHelper.raise(2) end if not mw.ustring.find(s, pattern, nil, plain) then DebugHelper.raise(2) endend
--------------------------------------------------------------------------------- Checks an input string doesn't contain the expected string-- @param message optional description of the test-- @param plain search is made with a plain string instead of a ustring pattern-- function ScribuntoUnit:assertNotStringContains(pattern, s, plain, message) if type(pattern) ~= 'string' then DebugHelper.raise(2) end if type(s) ~= 'string' then DebugHelper.raise(2) end local i, j = mw.ustring.find(s, pattern, nil, plain) if i then local match = mw.ustring.sub(s, i, j) DebugHelper.raise(2) endend
--------------------------------------------------------------------------------- Checks that an input has the expected value.-- @param message optional description of the test-- @example assertEquals(4, add(2,2), "2+2 should be 4")-- function ScribuntoUnit:assertEquals(expected, actual, message)
if type(expected)
'number' then self:assertWithinDelta(expected, actual, 1e-8, message)
elseif expected ~= actual then DebugHelper.raise(2) end
end
--------------------------------------------------------------------------------- Checks that 'actual' is within 'delta' of 'expected'.-- @param message optional description of the test-- @example assertEquals(1/3, 9/3, "9/3 should be 1/3", 0.000001)function ScribuntoUnit:assertWithinDelta(expected, actual, delta, message) if type(expected) ~= "number" then DebugHelper.raise(2) end if type(actual) ~= "number" then DebugHelper.raise(2) end local diff = expected - actual if diff < 0 then diff = - diff end -- instead of importing math.abs if diff > delta then DebugHelper.raise(2) endend
--------------------------------------------------------------------------------- Checks that a table has the expected value (including sub-tables).-- @param message optional description of the test-- @example assertDeepEquals(partition(odd,))function ScribuntoUnit:assertDeepEquals(expected, actual, message) if not DebugHelper.deepCompare(expected, actual) then if type(expected)
'table' then actual = mw.dumpObject(actual) end DebugHelper.raise(2) endend
--------------------------------------------------------------------------------- Checks that a wikitext gives the expected result after processing.-- @param message optional description of the test-- @example assertResultEquals("Hello world", "")function ScribuntoUnit:assertResultEquals(expected, text, message) local frame = self.frame local actual = frame:preprocess(text) if expected ~= actual then DebugHelper.raise(2) endend
--------------------------------------------------------------------------------- Checks that two wikitexts give the same result after processing.-- @param message optional description of the test-- @example assertSameResult("", "")function ScribuntoUnit:assertSameResult(text1, text2, message) local frame = self.frame local processed1 = frame:preprocess(text1) local processed2 = frame:preprocess(text2) if processed1 ~= processed2 then DebugHelper.raise(2) endend
--------------------------------------------------------------------------------- Checks that a parser function gives the expected output.-- @param message optional description of the test-- @example assertParserFunctionEquals("Hello world", "msg:concat",)function ScribuntoUnit:assertParserFunctionEquals(expected, pfname, args, message) local frame = self.frame local actual = frame:callParserFunction if expected ~= actual then DebugHelper.raise(2) endend
--------------------------------------------------------------------------------- Checks that a template gives the expected output.-- @param message optional description of the test-- @example assertTemplateEquals("Hello world", "concat",)function ScribuntoUnit:assertTemplateEquals(expected, template, args, message) local frame = self.frame local actual = frame:expandTemplate if expected ~= actual then DebugHelper.raise(2) endend
--------------------------------------------------------------------------------- Checks whether a function throws an error-- @param fn the function to test-- @param expectedMessage optional the expected error message-- @param message optional description of the testfunction ScribuntoUnit:assertThrows(fn, expectedMessage, message) local succeeded, actualMessage = pcall(fn) if succeeded then DebugHelper.raise(2) end -- For strings, strip the line number added to the error message actualMessage = type(actualMessage)
--------------------------------------------------------------------------------- Creates a new test suite.-- @param o a table with test functions (alternatively, the functions can be added later to the returned suite)-- function ScribuntoUnit:new(o) o = o or setmetatable(o,) o.run = function(frame) return self:run(o, frame) end return oend
--------------------------------------------------------------------------------- Resets global counters-- function ScribuntoUnit:init(frame) self.frame = frame or mw.getCurrentFrame self.successCount = 0 self.failureCount = 0 self.skipCount = 0 self.results = end
--------------------------------------------------------------------------------- Runs a single testcase-- @param name test nume-- @param test function containing assertions-- function ScribuntoUnit:runTest(suite, name, test) local success, details = pcall(test, suite) if success then self.successCount = self.successCount + 1 table.insert(self.results,) elseif type(details) ~= 'table' or not details.ScribuntoUnit then -- a real error, not a failed assertion self.failureCount = self.failureCount + 1 table.insert(self.results,) elseif details.skipped then self.skipCount = self.skipCount + 1 table.insert(self.results,) else self.failureCount = self.failureCount + 1 local message = details.source if details.message then message = message .. details.message .. "\n" end message = message .. details.text table.insert(self.results,) endend
--------------------------------------------------------------------------------- Runs all tests and displays the results.-- function ScribuntoUnit:runSuite(suite, frame) self:init(frame) local names = for name in pairs(suite) do if name:find('^test') then table.insert(names, name) end end table.sort(names) -- Put tests in alphabetical order. for i, name in ipairs(names) do local func = suite[name] self:runTest(suite, name, func) end return end
--------------------------------------------------------------------------------- #invoke entry point for running the tests.-- Can be called without a frame, in which case it will use mw.log for output-- @param displayMode see displayResults-- function ScribuntoUnit:run(suite, frame) local testData = self:runSuite(suite, frame) if frame and frame.args then return self:displayResults(testData, frame.args.displayMode or 'table') else return self:displayResults(testData, 'log') endend
--------------------------------------------------------------------------------- Displays test results -- @param displayMode: 'table', 'log' or 'short'-- function ScribuntoUnit:displayResults(testData, displayMode) if displayMode
'log' then return self:displayResultsAsLog(testData) elseif displayMode
function ScribuntoUnit:displayResultsAsLog(testData) if testData.failureCount > 0 then mw.log('FAILURES!!!') elseif testData.skipCount > 0 then mw.log('Some tests could not be executed without a frame and have been skipped. Invoke this test suite as a template to run all tests.') end mw.log(string.format('Assertions: success: %d, error: %d, skipped: %d', testData.successCount, testData.failureCount, testData.skipCount)) mw.log('-------------------------------------------------------------------------------') for _, result in ipairs(testData.results) do if result.error then mw.log(string.format('%s: %s', result.name, result.message)) end endend
function ScribuntoUnit:displayResultsAsShort(testData) local text = string.format(cfg.shortResultsFormat, testData.successCount, testData.failureCount, testData.skipCount) if testData.failureCount > 0 then text = '
' .. text .. '' end return textendfunction ScribuntoUnit:displayResultsAsTable(testData) local successIcon, failIcon = self.frame:preprocess(cfg.successIndicator), self.frame:preprocess(cfg.failureIndicator) local text = if testData.failureCount > 0 then local msg = mw.message.newRawMessage(cfg.failureSummary, testData.failureCount):plain msg = self.frame:preprocess(msg) if cfg.failureCategory then msg = cfg.failureCategory .. msg end text = text .. failIcon .. ' ' .. msg .. '\n' else text = text .. successIcon .. ' ' .. cfg.successSummary .. '\n' end text = text .. '
\n! ' .. cfg.nameString .. '\n | ' .. cfg.expectedString .. '\n! ' .. cfg.actualString .. '\n' for _, result in ipairs(testData.results) do text = text .. ' | -\n' if result.error then text = text .. ' | ' .. failIcon .. '\n | ' if (result.expected and result.actual) then local name = result.name if result.testname then name = name .. ' / ' .. result.testname end text = text .. name .. '\n | ' .. mw.text.nowiki(tostring(result.expected)) .. '\n | ' .. mw.text.nowiki(tostring(result.actual)) .. '\n' else text = text .. result.name .. '\n | ' .. ' colspan="2" | ' .. mw.text.nowiki(result.message) .. '\n' end else text = text .. ' | ' .. successIcon .. '\n | ' .. result.name .. '\n | \n | \n' end end text = text .. ' |
---|
return ScribuntoUnit