Модуль: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">< ' .. mw.text.nowiki(prevText1) ..
'<del class="diffchange-inline">' .. mw.text.nowiki(old) .. '</del>' ..
mw.text.nowiki(nextTextOld) .. '</div>' ..
'<div class="diff-addedline">> ' .. 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