Модуль:TemplateTester (Bk;rl,&TemplateTester)

Перейти к навигации Перейти к поиску
Документация

Для тестирования шаблонов необходимо пользоваться шаблоном {{TemplateTester}}, являющимся абстракцией над данным модулем. Документацию по использованию модуля для тестирования шаблонов указана в шаблоне.

Методы модуля

[править код]
  • test

Метод загружает указанный в качестве аргумента testFile модуль с наборами тестов и запускает их на исполнение. Возвращает HTML-таблицу с исходными данными и результатами тестирования. Пример вызова:

{{#invoke:TemplateTester|test
 |testFile = Модуль:Название модуля/testcases
}}

Для того, чтобы вывести только проваленные тесты можно задать аргумент (флаг) failedOnly:

{{#invoke:TemplateTester|test
 |testFile = Модуль:Название модуля/testcases
 |failedOnly = 1
}}

Разработка

[править код]

См. также

[править код]
  • Модуль:UnitTests — тредиционный модуль для тестирования шаблонов.
  • Модуль:ScribuntoUnit — модуль для тестирования других модулей, написанных на Lua.
require('strict')

local p = {}

local NS_MODULE = 828 --: https://www.mediawiki.org/wiki/Extension_default_namespaces
local moduleNamespace = mw.site.namespaces[NS_MODULE].name

local formatter = require(moduleNamespace .. ':WDFormat')
local f = formatter.f
local diff = require(moduleNamespace .. ':Diff')

local langObj = mw.language.getContentLanguage()

local i18n = require(moduleNamespace .. ':I18n')

local l10n = i18n.load()

local function formatSingleDiff(prevText1, prevText2, old, new, nextTextOld, nextTextNew)
	return '<div class="diff-deletedline">&lt; ' .. mw.text.nowiki(prevText1) ..
		'<del class="diffchange-inline">' .. mw.text.nowiki(old) .. '</del>' ..
		mw.text.nowiki(nextTextOld) .. '</div>' ..
		'<div class="diff-addedline">&gt; ' .. mw.text.nowiki(prevText2) ..
		'<ins class="diffchange-inline">' .. mw.text.nowiki(new) .. '</ins>' ..
		mw.text.nowiki(nextTextNew) .. '</div>'
end

local function findWordStartBound(text, pos)
	local i = pos - 1
	while i > 0 do
		local c = mw.ustring.sub(text, i, i)
		if mw.ustring.match(c, "[ \t\n>%]]") then
			return i + 1
		end
		if mw.ustring.match(c, "[%[<]") then
			return i
		end
		i = i - 1
	end
	return 1
end

local function findWordEndBound(text, pos)
	local i = pos + 1
	while i <= mw.ustring.len(text) do
		local c = mw.ustring.sub(text, i, i)
		if mw.ustring.match(c, "[ \t\n%[<]") then
			return i - 1
		end
		if mw.ustring.match(c, "[>%]]") then
			return i
		end
		i = i + 1
	end
	return 1
end

local function formatDiffs(expected, processed, isPattern)
	if expected == nil and processed == nil then
		return ''
	end

	local diffsStr = '<div class="diff-in-test">'
	if processed == nil then
		diffsStr = diffsStr .. formatSingleDiff('', expected, '', '')
	elseif expected == nil then
		if isPattern then
			processed = mw.ustring.gsub(processed, '%%([%%%.%-%[%]%(%)])', function (s)
				return s
			end)
		end
		diffsStr = diffsStr .. formatSingleDiff('', '', processed, '')
	else
		local diffs = diff.fastStrDiffRanges(expected, processed)
		for i, d in ipairs(diffs) do
			local s1 = ''
			if d[1][2] > 0 then
				s1 = mw.ustring.sub(expected, d[1][1], d[1][1] + d[1][2] - 1)
			end
			local s2 = ''
			if d[2][2] > 0 then
				s2 = mw.ustring.sub(processed, d[2][1], d[2][1] + d[2][2] - 1)
			end
			local s1 = ''
			local s2 = ''
			local s1End = d[1][1] + d[1][2]
			local s2End = d[2][1] + d[2][2]
			s1End = s1End - 1
			s2End = s2End - 1
			if d[1][2] > 0 then
				s1 = mw.ustring.sub(expected, d[1][1], s1End)
			end
			if d[2][2] > 0 then
				s2 = mw.ustring.sub(processed, d[2][1], s2End)
			end
			local wordStart1 = findWordStartBound(expected, d[1][1])
			local wordStart2 = findWordStartBound(processed, d[2][1])
			local prevText1 = mw.ustring.sub(expected, wordStart1, d[1][1] - 1)
			local prevText2 = mw.ustring.sub(processed, wordStart2, d[2][1] - 1)
			local wordEnd1 = findWordEndBound(expected, s1End)
			local wordEnd2 = findWordEndBound(processed, s2End)
			local nextText1 = ''
			if s1End > 0 then
				nextText1 = mw.ustring.sub(expected, s1End + 1, wordEnd1)
			end
			local nextText2 = ''
			if s2End > 0 then
				nextText2 = mw.ustring.sub(processed, s2End + 1, wordEnd2)
			end
			if isPattern then
				prevText2 = mw.ustring.gsub(prevText2, '%%([%%%.%-%[%]%(%)])', function (s)
					return s
				end)
				s2 = mw.ustring.gsub(s2, '%%([%%%.%-%[%]%(%)])', function (s)
					return s
				end)
				nextText2 = mw.ustring.gsub(nextText2, '%%([%%%.%-%[%]%(%)])', function (s)
					return s
				end)
			end
			diffsStr = diffsStr .. formatSingleDiff(prevText1, prevText2, s1, s2, nextText1, nextText2)
			if i > 4 then
				break
			end
		end
	end

	diffsStr = diffsStr .. '</div>'
	return diffsStr
end

local function preprocessTests(frame, tests, failedOnly)
	local passed = 0
	for _, test in ipairs(tests) do
		test.input = test.input
		test.output = test.output
		test.actualOutput = frame:preprocess(test.input) or tostring(test.input) or ''
		if test.pattern then
			if mw.ustring.find(test.actualOutput, test.pattern) then
				test.state = true
			else
				test.state = false
			end
		elseif test.output then
			test.state = (test.output == test.actualOutput)
		else
			test.state = false
		end
		test.diff = ''
		if not test.state then
			local output = test.pattern or test.output
			if output then
				local actualOutput = test.actualOutput
				if test.pattern and test.diffByPattern then
					actualOutput = mw.ustring.gsub(actualOutput, '[%%%.%-%[%]]', function (c)
						return '%' .. c
					end)
				end
				test.diff = formatDiffs(
					output,
					actualOutput,
					test.diffByPattern
				)
			end
		end
		if test.state then
			passed = passed + 1
		end

		test.visible = true
		if test.state and failedOnly then
			test.visible = false
		end
	end
	return passed
end

local function argIsEmpty(s)
	return (s == nil or s == '')
end

local function valueToTable(value)
	if value == nil then
		return nil
	end
	return { value = value }
end

local function testsToFormatterTable(tests)
	for _, test in ipairs(tests) do
		for key, value in pairs(test) do
			test[key] = valueToTable(value)
		end
	end
end

local headerTag = {
	name = 'th',
}

local rowTag = {
	name = 'tr',
}

local cellTag = {
	name = 'td',
}

local cellErrorTag = {
	name = 'td',
	tag = {
		name = 'span',
		classes = { 'error' },
	},
}

local function formatState(source, processedData, result)
	if source.state.value then
		result.text = '✅'
	else 
		result.text = '❌'
	end
	result.wikitext = result.text
end

local function formatSuiteTitle(source, processedData, result)
	result.text = mw.ustring.format(l10n('suite-stats'), result.text, source.passed.value, source.total.value)
	result.wikitext = result.text
end

local function formatCommentAndWikitext(source, processedData, result)
	if not source.comment or source.comment.value == '' then
		result.wikitext = mw.text.nowiki(source.input.value)
	else
		local frame = mw.getCurrentFrame()
		result.text = source.comment.value .. '\n' .. source.input.value
		result.wikitext = source.comment.value .. '<hr>' .. frame:extensionTag('syntaxhighlight', source.input.value, { lang = 'wikitext' })
	end
end

local function formatNoWiki(source, processedData, result)
	result.text = mw.text.nowiki(result.text)
	result.wikitext = result.text
end

local function filterVisible(source)
	if not source.visible.value then
		return false
	end
	return true
end

local function suiteTitleVisible(source, params)
	if source.passed.value == source.total.value and params.failedOnly then
		return false
	end
	return true
end

local ResultsProfile = {
	tag = {
		name = 'div',
	},
	{
		tag = {
			name = 'table',
			classes = { 'wikitable' },
			css = {
				width = '100 %',
			},
		},
		{
			tag = {
				name = 'tr',
				tag = {
					name = 'th',
					attr = { colspan = 5 },
				},
			},
			cond = suiteTitleVisible,
			field = 'name',
			capitalize = true,
			format = { formatSuiteTitle },
		},
		{
			tag = rowTag,
			{
				tag = headerTag,
				value = '',
			},
			{
				tag = headerTag,
				value = l10n('test'),
				capitalize = true,
			},
			{
				tag = headerTag,
				value = l10n('expected'),
				capitalize = true,
			},
			{
				tag = headerTag,
				value = l10n('actual'),
				capitalize = true,
			},
			{
				tag = headerTag,
				value = l10n('diff'),
				capitalize = true,
			},
			isStatic = true,
		},
		{
			tag = rowTag,
			cond = filterVisible,
			field = 'tests',
			{
				tag = cellTag,
				field = 'state',
				format = { formatState },
			},
			{
				tag = cellTag,
				field = 'input',
				format = { formatCommentAndWikitext },
			},
			{
				-- Display error message if wikitext isn't set
				conflicts = 'input',
				tag = cellErrorTag,
				value = l10n('error-no-wikitext'),
			},
			{
				conflicts = 'pattern',
				tag = cellTag,
				field = 'output',
			},
			{
				conflicts = 'output',
				tag = cellTag,
				field = 'pattern',
			},
			{
				-- Display error message if output or
				-- pattern aren't set
				conflicts = { 'output', 'pattern' },
				tag = cellErrorTag,
				value = l10n('error-no-expected-or-pattern'),
			},
			{
				tag = cellTag,
				field = 'actualOutput',
			},
			{
				tag = cellTag,
				field = 'diff',
			},
		},
	},
}

function p.test(frame)
	local args = frame.args
	local failedOnly = not argIsEmpty(args.failedOnly)

	local suites = require(args.testFile)
	local passed = 0
	local total = 0
	for _, suite in ipairs(suites) do
		suite.passed = preprocessTests(frame, suite.tests, failedOnly)
		suite.total = table.getn(suite.tests)

		passed = passed + suite.passed
		total = total + suite.total

		testsToFormatterTable(suite.tests)
		suite.name = valueToTable(suite.name)
		suite.passed = valueToTable(suite.passed)
		suite.total = valueToTable(suite.total)
	end
	if passed == total then
		return mw.ustring.format(l10n('all-tests-passed'), args.testFile)
	end
	
	local params = {
		failedOnly = failedOnly,
	}

	local selfTitle = frame:getTitle()
	local styles = frame:extensionTag('templatestyles', '', { src = selfTitle .. '/styles.css' })
	local passedInfo = mw.ustring.format(l10n('all-tests-stats'), args.testFile, passed, total, total - passed)
	return styles .. '<div class="mw-collapsible mw-collapsed">' .. passedInfo .. '<div class="mw-collapsible-content" style="overflow: auto;">' .. formatter.format(ResultsProfile, suites, nil, params) .. '</div></div>'
end

return p