Jump to content
Main menu
Main menu
move to sidebar
hide
Navigation
Main page
Recent changes
Random page
Help about MediaWiki
Humanipedia
Search
Search
Appearance
Create account
Log in
Personal tools
Create account
Log in
Pages for logged out editors
learn more
Contributions
Talk
Editing
Module:ScribuntoUnit
Module
Discussion
English
Read
Edit source
View history
Tools
Tools
move to sidebar
hide
Actions
Read
Edit source
View history
General
What links here
Related changes
Special pages
Page information
Appearance
move to sidebar
hide
Warning:
You are not logged in. Your IP address will be publicly visible if you make any edits. If you
log in
or
create an account
, your edits will be attributed to your username, along with other benefits.
Anti-spam check. Do
not
fill this in!
------------------------------------------------------------------------------- -- 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}, ' => ', ', ') => "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 == i and unnamedArguments then i = i + 1 concatted = concatted .. tostring(v) else unnamedArguments = false concatted = concatted .. tostring(k) .. keySeparator .. tostring(v) end end return concatted end ------------------------------------------------------------------------------- -- 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 == t2 end local metatable = getmetatable(t1) if not ignoreMetatable and metatable and metatable.__eq then return t1 == t2 end for k1, v1 in pairs(t1) do local v2 = t2[k1] if v2 == nil or not DebugHelper.deepCompare(v1, v2) then return false end end for k2, v2 in pairs(t2) do if t1[k2] == nil then return false end end return true end ------------------------------------------------------------------------------- -- 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, { -- __tostring: function() return details.text end -- }) 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({ScribuntoUnit = true, skipped = true}, 3) end ------------------------------------------------------------------------------- -- Unconditionally fail a test -- @param message optional description of the test -- function ScribuntoUnit:fail(message) DebugHelper.raise({ScribuntoUnit = true, text = "Test failed", message = message}, 2) 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({ScribuntoUnit = true, text = string.format("Failed to assert that %s is true", tostring(actual)), message = message}, 2) end end ------------------------------------------------------------------------------- -- Checks that the input is false -- @param message optional description of the test -- function ScribuntoUnit:assertFalse(actual, message) if actual then DebugHelper.raise({ScribuntoUnit = true, text = string.format("Failed to assert that %s is false", tostring(actual)), message = message}, 2) end end ------------------------------------------------------------------------------- -- 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({ ScribuntoUnit = true, text = mw.ustring.format("Pattern type error (expected string, got %s)", type(pattern)), message = message }, 2) end if type(s) ~= 'string' then DebugHelper.raise({ ScribuntoUnit = true, text = mw.ustring.format("String type error (expected string, got %s)", type(s)), message = message }, 2) end if not mw.ustring.find(s, pattern, nil, plain) then DebugHelper.raise({ ScribuntoUnit = true, text = mw.ustring.format('Failed to find %s "%s" in string "%s"', plain and "plain string" or "pattern", pattern, s), message = message }, 2) end end ------------------------------------------------------------------------------- -- 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({ ScribuntoUnit = true, text = mw.ustring.format("Pattern type error (expected string, got %s)", type(pattern)), message = message }, 2) end if type(s) ~= 'string' then DebugHelper.raise({ ScribuntoUnit = true, text = mw.ustring.format("String type error (expected string, got %s)", type(s)), message = message }, 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({ ScribuntoUnit = true, text = mw.ustring.format('Found match "%s" for %s "%s"', match, plain and "plain string" or "pattern", pattern), message = message }, 2) end end ------------------------------------------------------------------------------- -- 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' and type(actual) == 'number' then self:assertWithinDelta(expected, actual, 1e-8, message) elseif expected ~= actual then DebugHelper.raise({ ScribuntoUnit = true, text = string.format("Failed to assert that %s equals expected %s", tostring(actual), tostring(expected)), actual = actual, expected = expected, message = message, }, 2) end end ------------------------------------------------------------------------------- -- Checks that an input does not have the expected value. -- @param message optional description of the test -- @example assertNotEquals(5, add(2,2), "2+2 should not be 5") -- function ScribuntoUnit:assertNotEquals(expected, actual, message) if type(expected) == 'number' and type(actual) == 'number' then self:assertNotWithinDelta(expected, actual, 1e-8, message) elseif expected == actual then DebugHelper.raise({ ScribuntoUnit = true, text = string.format("Failed to assert that %s does not equal expected %s", tostring(actual), tostring(expected)), actual = actual, expected = expected, message = message, }, 2) end end ------------------------------------------------------------------------------- -- Validates that both the expected and actual values are numbers -- @param message optional description of the test -- local function validateNumbers(expected, actual, message) if type(expected) ~= "number" then DebugHelper.raise({ ScribuntoUnit = true, text = string.format("Expected value %s is not a number", tostring(expected)), actual = actual, expected = expected, message = message, }, 3) end if type(actual) ~= "number" then DebugHelper.raise({ ScribuntoUnit = true, text = string.format("Actual value %s is not a number", tostring(actual)), actual = actual, expected = expected, message = message, }, 3) end end ------------------------------------------------------------------------------- -- Checks that 'actual' is within 'delta' of 'expected'. -- @param message optional description of the test -- @example assertWithinDelta(1/3, 3/9, 0.000001, "3/9 should be 1/3") function ScribuntoUnit:assertWithinDelta(expected, actual, delta, message) validateNumbers(expected, actual, message) local diff = expected - actual if diff < 0 then diff = - diff end -- instead of importing math.abs if diff > delta then DebugHelper.raise({ ScribuntoUnit = true, text = string.format("Failed to assert that %f is within %f of expected %f", actual, delta, expected), actual = actual, expected = expected, message = message, }, 2) end end ------------------------------------------------------------------------------- -- Checks that 'actual' is not within 'delta' of 'expected'. -- @param message optional description of the test -- @example assertNotWithinDelta(1/3, 2/3, 0.000001, "1/3 should not be 2/3") function ScribuntoUnit:assertNotWithinDelta(expected, actual, delta, message) validateNumbers(expected, actual, message) local diff = expected - actual if diff < 0 then diff = - diff end -- instead of importing math.abs if diff <= delta then DebugHelper.raise({ ScribuntoUnit = true, text = string.format("Failed to assert that %f is not within %f of expected %f", actual, delta, expected), actual = actual, expected = expected, message = message, }, 2) end end ------------------------------------------------------------------------------- -- Checks that a table has the expected value (including sub-tables). -- @param message optional description of the test -- @example assertDeepEquals({{1,3}, {2,4}}, partition(odd, {1,2,3,4})) function ScribuntoUnit:assertDeepEquals(expected, actual, message) if not DebugHelper.deepCompare(expected, actual) then if type(expected) == 'table' then expected = mw.dumpObject(expected) end if type(actual) == 'table' then actual = mw.dumpObject(actual) end DebugHelper.raise({ ScribuntoUnit = true, text = string.format("Failed to assert that %s equals expected %s", tostring(actual), tostring(expected)), actual = actual, expected = expected, message = message, }, 2) end end ------------------------------------------------------------------------------- -- Checks that a wikitext gives the expected result after processing. -- @param message optional description of the test -- @example assertResultEquals("Hello world", "{{concat|Hello|world}}") function ScribuntoUnit:assertResultEquals(expected, text, message) local frame = self.frame local actual = frame:preprocess(text) if expected ~= actual then DebugHelper.raise({ ScribuntoUnit = true, text = string.format("Failed to assert that %s equals expected %s after preprocessing", text, tostring(expected)), actual = actual, actualRaw = text, expected = expected, message = message, }, 2) end end ------------------------------------------------------------------------------- -- Checks that two wikitexts give the same result after processing. -- @param message optional description of the test -- @example assertSameResult("{{concat|Hello|world}}", "{{deleteLastChar|Hello world!}}") 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({ ScribuntoUnit = true, text = string.format("Failed to assert that %s equals expected %s after preprocessing", processed1, processed2), actual = processed1, actualRaw = text1, expected = processed2, expectedRaw = text2, message = message, }, 2) end end ------------------------------------------------------------------------------- -- Checks that a parser function gives the expected output. -- @param message optional description of the test -- @example assertParserFunctionEquals("Hello world", "msg:concat", {"Hello", " world"}) function ScribuntoUnit:assertParserFunctionEquals(expected, pfname, args, message) local frame = self.frame local actual = frame:callParserFunction{ name = pfname, args = args} if expected ~= actual then DebugHelper.raise({ ScribuntoUnit = true, text = string.format("Failed to assert that %s with args %s equals expected %s after preprocessing", DebugHelper.concatWithKeys(args), pfname, expected), actual = actual, actualRaw = pfname, expected = expected, message = message, }, 2) end end ------------------------------------------------------------------------------- -- Checks that a template gives the expected output. -- @param message optional description of the test -- @example assertTemplateEquals("Hello world", "concat", {"Hello", " world"}) function ScribuntoUnit:assertTemplateEquals(expected, template, args, message) local frame = self.frame local actual = frame:expandTemplate{ title = template, args = args} if expected ~= actual then DebugHelper.raise({ ScribuntoUnit = true, text = string.format("Failed to assert that %s with args %s equals expected %s after preprocessing", DebugHelper.concatWithKeys(args), template, expected), actual = actual, actualRaw = template, expected = expected, message = message, }, 2) end end ------------------------------------------------------------------------------- -- 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 test function ScribuntoUnit:assertThrows(fn, expectedMessage, message) local succeeded, actualMessage = pcall(fn) if succeeded then DebugHelper.raise({ ScribuntoUnit = true, text = 'Expected exception but none was thrown', message = message, }, 2) end -- For strings, strip the line number added to the error message actualMessage = type(actualMessage) == 'string' and string.match(actualMessage, 'Module:[^:]*:[0-9]*: (.*)') or actualMessage local messagesMatch = DebugHelper.deepCompare(expectedMessage, actualMessage) if expectedMessage and not messagesMatch then DebugHelper.raise({ ScribuntoUnit = true, expected = expectedMessage, actual = actualMessage, text = string.format('Expected exception with message %s, but got message %s', tostring(expectedMessage), tostring(actualMessage) ), message = message }, 2) end end ------------------------------------------------------------------------------- -- Checks whether a function doesn't throw an error -- @param fn the function to test -- @param message optional description of the test function ScribuntoUnit:assertDoesNotThrow(fn, message) local succeeded, actualMessage = pcall(fn) if succeeded then return end -- For strings, strip the line number added to the error message actualMessage = type(actualMessage) == 'string' and string.match(actualMessage, 'Module:[^:]*:[0-9]*: (.*)') or actualMessage DebugHelper.raise({ ScribuntoUnit = true, actual = actualMessage, text = string.format('Expected no exception, but got exception with message %s', tostring(actualMessage) ), message = message }, 2) end ------------------------------------------------------------------------------- -- 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 {} o._tests = {} setmetatable(o, { __index = self, __newindex = function (t, k, v) if type(k) == "string" and k:find('^test') and type(v) == "function" then -- Store test functions in the order they were defined table.insert(o._tests, {name = k, test = v}) else rawset(t, k, v) end end }) o.run = function(frame) return self:run(o, frame) end return o end ------------------------------------------------------------------------------- -- 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, {name = name, success = true}) 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, {name = name, error = true, message = 'Lua error -- ' .. tostring(details)}) elseif details.skipped then self.skipCount = self.skipCount + 1 table.insert(self.results, {name = name, skipped = true}) else self.failureCount = self.failureCount + 1 local message = details.source or "" if details.message then message = message .. details.message .. "\n" end message = message .. details.text table.insert(self.results, {name = name, error = true, message = message, expected = details.expected, actual = details.actual, testname = details.message}) end end ------------------------------------------------------------------------------- -- Runs all tests and displays the results. -- function ScribuntoUnit:runSuite(suite, frame) self:init(frame) for i, testDetails in ipairs(suite._tests) do self:runTest(suite, testDetails.name, testDetails.test) end return { successCount = self.successCount, failureCount = self.failureCount, skipCount = self.skipCount, results = self.results, } 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') end end ------------------------------------------------------------------------------- -- Displays test results -- @param displayMode: 'table', 'log' or 'short' -- function ScribuntoUnit:displayResults(testData, displayMode) if displayMode == 'table' then return self:displayResultsAsTable(testData) elseif displayMode == 'log' then return self:displayResultsAsLog(testData) elseif displayMode == 'short' then return self:displayResultsAsShort(testData) else error('unknown display mode') end end 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 end end function ScribuntoUnit:displayResultsAsShort(testData) local text = string.format(cfg.shortResultsFormat, testData.successCount, testData.failureCount, testData.skipCount) if testData.failureCount > 0 then text = '<span class="error">' .. text .. '</span>' end return text end function 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 .. '{| class="wikitable scribunto-test-table"\n' 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 .. mw.text.nowiki(name) .. '\n| ' .. mw.text.nowiki(tostring(result.expected)) .. '\n| ' .. mw.text.nowiki(tostring(result.actual)) .. '\n' else text = text .. mw.text.nowiki(result.name) .. '\n| ' .. ' colspan="2" | ' .. mw.text.nowiki(result.message) .. '\n' end else text = text .. '| ' .. successIcon .. '\n| ' .. mw.text.nowiki(result.name) .. '\n|\n|\n' end end text = text .. '|}\n' return text end return ScribuntoUnit
Summary:
Please note that all contributions to Humanipedia may be edited, altered, or removed by other contributors. If you do not want your writing to be edited mercilessly, then do not submit it here.
You are also promising us that you wrote this yourself, or copied it from a public domain or similar free resource (see
Humanipedia:Copyrights
for details).
Do not submit copyrighted work without permission!
Cancel
Editing help
(opens in new window)
Template used on this page:
Module:ScribuntoUnit/doc
(
edit
)