Módulo:ScribuntoUnit

Fonte: Enciclopédia de conhecimento da Igreja de Deus
Revisão em 01h24min de 14 de maio de 2024 por Jaewoo (discussão | contribs) (Criou a página com "------------------------------------------------------------------------------- -- Unit tests for Scribunto. ------------------------------------------------------------------------------- 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('Módulo:ScribuntoUnit/config') -----------------------------------------------------...")
(dif) ← Revisão anterior | Revisão atual (dif) | Revisão seguinte → (dif)
Saltar para a navegação Saltar para a pesquisa

A documentação para este módulo pode ser criada na página Módulo:ScribuntoUnit/doc

-------------------------------------------------------------------------------
-- Unit tests for Scribunto.
-------------------------------------------------------------------------------
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('Módulo:ScribuntoUnit/config')
-------------------------------------------------------------------------------
-- Concatenates keys and values, ideal for displaying a template 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
-------------------------------------------------------------------------------
-- 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("Falhou ao declarar que %s é verdadeiro(a)", 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("Falhou ao declarar que %s é falso(a)", 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("Erro do tipo de padrão (esperava string, recebeu %s)", type(pattern)),
			message = message
		}, 2)
	end
	if type(s) ~= 'string' then
		DebugHelper.raise({
			ScribuntoUnit = true,
			text = mw.ustring.format("Erro do tipo de string (esperava string, recebeu %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('Falhou ao encontrar %s "%s" no string "%s"', plain and "apenas string" or "padrão", 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("Erro do tipo de padrão (esperava string, recebeu %s)", type(pattern)),
			message = message
		}, 2)
	end
	if type(s) ~= 'string' then
		DebugHelper.raise({
			ScribuntoUnit = true,
			text = mw.ustring.format("Erro do tipo de string (esperava string, recebeu %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('Equivalente encontrado "%s" para %s "%s"', match, plain and "apenas string" or "padrão", 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("Falhou ao declarar que %s é igual ao(à) %s esperado(a)", tostring(actual), tostring(expected)), 
												actual = actual,
												expected = expected,
												message = message,
								}, 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({
												ScribuntoUnit = true,
												text = string.format("Valor esperado %s não é um número", tostring(expected)),
												actual = actual,
												expected = expected,
												message = message,
								}, 2)
				end
				if type(actual) ~= "number" then
								DebugHelper.raise({
												ScribuntoUnit = true,
												text = string.format("Valor na realidade %s não é um número", tostring(actual)),
												actual = actual,
												expected = expected,
												message = message,
								}, 2)
				end
				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("Falhou ao declarar que %f está no(a) %f de %f esperado(a)(s)", 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("Falhou ao declarar que %s é igual ao(à) %s esperado(a)", 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("Falhou ao declarar que %s é igual ao(à) %s esperado depois de pré-processar", 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("Falhou ao declarar que %s é igual ao(a) %s esperado(a) depois de pré-processar", processed1, processed2), 
												actual = processed1,
												actualRaw = text1,
												expected = processed2,
												expectedRaw = text2,
												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("Falhou ao declarar que %s com args %s é igual ao(à) %s esperado depois de pré-processar", 
																																	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 = 'Exceção esperada, mas nenhuma foi dada',
												message = message,
								}, 2)
				end
	-- For strings, strip the line number added to the error message
				actualMessage = type(actualMessage) == 'string' 
					and string.match(actualMessage, 'Módulo:[^:]*:[0-9]*: (.*)')
					or actualMessage
				local messagesMatch = DebugHelper.deepCompare(expectedMessage, actualMessage)
				if expectedMessage and not messagesMatch then
								DebugHelper.raise({
												ScribuntoUnit = true,
												expected = expected,
												actual = actual,
												text = string.format('Exceção esperada com mensagem %s, mas recebeu mensagem %s', 
																tostring(expectedMessage), tostring(actualMessage)
												),
												message = message
								}, 2)
				end
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 {}
				setmetatable(o, {__index = self})
				o.run = function(frame) return self:run(o, frame) end
				return o
end
-------------------------------------------------------------------------------
-- Resets global counters
-- 
function ScribuntoUnit:init(frame)
				self.frame = frame
				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 = 'Erro de Lua -- ' .. 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
								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})
				end
end
-------------------------------------------------------------------------------
-- 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 {
								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 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('FALHAS!!!')
				elseif testData.skipCount > 0 then
								mw.log('Alguns testes não puderam ser executados sem um frame e foram pulados. Invoque esta suite de testes como uma predefinição para executar todos os testes.')
				end
				mw.log(string.format('Declarações: sucessos: %d, erros: %d, pulados: %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| ' .. result.name .. '\n| '
												if (result.expected and result.actual) then
																text = text .. mw.text.nowiki(tostring(result.expected)) .. '\n| ' .. mw.text.nowiki(tostring(result.actual)) .. '\n'
												else
																text = text .. ' colspan="2" | ' .. mw.text.nowiki(result.message) .. '\n'
												end
								else
												text = text .. '| ' .. successIcon .. '\n| ' .. result.name .. '\n|\n|\n'
								end
				end
				text = text .. '|}\n'
				return text
end
return ScribuntoUnit