Module:Convert/tester
-- Test the output from a template by comparing it with fixed text.
-- The expected text must be in a single line, but can include
-- "\n" (two characters) to indicate that a newline is expected.
-- Tests are run (or created) by setting p.tests (string or table), or
-- by setting page=PAGE_TITLE (and optionally section=SECTION_TITLE),
-- then executing run_tests (or make_tests).
local Collection = {}
Collection.__index = Collection
do
function Collection:add(item)
if item ~= nil then
self.n = self.n + 1
self[self.n] = item
end
end
function Collection:join(sep)
return table.concat(self, sep)
end
function Collection.new()
return setmetatable({n = 0}, Collection)
end
end
local function empty(text)
-- Return true if text is nil or empty (assuming a string).
return text == nil or text == ''
end
local function strip(text)
-- Return text with no leading/trailing whitespace.
return text:match("^%s*(.-)%s*$")
end
local function normalize(text)
-- Return text with any strip markers normalized by replacing the
-- unique number with a fixed value so comparisons work.
return text:gsub('(\127[^\127]*UNIQ[^\127]*%-)(%x\+)(-QINU[^\127]*\127)', '%100000000%3')
end
local function status_box(stats, expected, actual, iscomment)
local label, bgcolor, align, isfail
if iscomment then
actual = ''
align = 'center'
bgcolor = 'silver'
label = 'Cmnt'
elseif expected == '' then
stats.ignored = stats.ignored + 1
return '', actual
elseif normalize(expected) == normalize(actual) then
stats.pass = stats.pass + 1
actual = ''
align = 'center'
bgcolor = 'green'
label = 'Pass'
else
stats.fail = stats.fail + 1
align = 'center'
bgcolor = 'red'
label = 'Fail'
isfail = true
end
local sbox = 'style="text-align:' .. align .. ';color:white;background:' .. bgcolor .. ';" | ' .. label
return sbox, actual, isfail
end
local function status_text(stats)
local bgcolor, ignored_text, msg, ttext
if stats.template then
ttext = "'''Using [[Template:" .. stats.template .. "]]:''' "
else
ttext = ''
end
if stats.fail == 0 then
if stats.pass == 0 then
bgcolor = 'salmon'
msg = 'No tests performed'
else
bgcolor = 'green'
msg = string.format('All %d tests passed', stats.pass)
end
else
bgcolor = 'darkred'
msg = string.format('%d test%s failed', stats.fail, stats.fail == 1 and '' or 's')
end
if stats.ignored == 0 then
ignored_text = ''
else
bgcolor = 'salmon'
ignored_text = string.format(', %d test%s ignored because expected text is blank', stats.ignored, stats.ignored == 1 and '' or 's')
end
return ttext .. '<span style="font-size:120%;color:white;background-color:' .. bgcolor .. ';">' ..
msg .. ignored_text .. '.</span>'
end
local function run_template(frame, template, args, collapse_multiline)
-- Template "{{ example | 2 = def | abc | name = ghi jkl }}"
-- gives xargs { " abc ", "def", name = "ghi jkl" }.
if template:sub(1, 2) == '{{' and template:sub(-2, -1) == '}}' then
template = template:sub(3, -3) .. '|' -- append sentinel to get last field
else
return '(invalid template)'
end
local xargs = {}
local index = 1
local templatename
local function put_arg(k, v)
-- Kludge: Module:Val uses Module:Arguments which trims arguments and
-- omits blank arguments. Simulate that here.
-- LATER Need a parameter to control this.
if templatename:sub(1, 3) == 'val' then
v = strip(v)
if v == '' then
return
end
end
xargs[k] = v
end
template = template:gsub('(%[%[[^%[%]]-)|(.-%]%])', '%1\0%2') -- replace pipe in piped link with a zero byte
for field in template:gmatch('(.-)|') do
field = field:gsub('%z', '|') -- restore pipe in piped link
if templatename == nil then
templatename = args.template or strip(field)
if templatename == '' then
return '(invalid template)'
end
else
local k, eq, v = field:match("^(.-)(=)(.*)$")
if eq then
k, v = strip(k), strip(v) -- k and/or v can be empty
local i = tonumber(k)
if i and i > 0 and string.match(k, '^%d+$') then
put_arg(i, v)
else
put_arg(k, v)
end
else
while xargs[index] ~= nil do
-- Skip any explicit numbered parameters like "|5=five".
index = index + 1
end
put_arg(index, field)
end
end
end
if args.test and not xargs.test then
-- For convert, allow test=preview or test=nopreview to be injected into
-- the convert under test, if it does not already use that parameter.
-- That allows, for example, a preview of make_tests to show nopreview results.
xargs.test = args.test
end
local function expand(t)
return frame:expandTemplate(t)
end
local ok, result = pcall(expand, { title = templatename, args = xargs })
if not ok then
result = 'Error: ' .. result
end
if collapse_multiline then
result = result:gsub('\n', '\\n')
end
return result
end
local function _make_tests(frame, all_tests, args)
local maxlen = 38
for _, item in ipairs(all_tests) do
local template = item[1]
if template then
local templen = mw.ustring.len(template)
item.templen = templen
if maxlen < templen and templen <= 70 then
maxlen = templen
end
end
end
local result = Collection.new()
for _, item in ipairs(all_tests) do
local template = item[1]
if template then
local actual = run_template(frame, template, args, true)
local pad = string.rep(' ', maxlen - item.templen) .. ' '
result:add(template .. pad .. actual)
else
local text = item.text
if text then
result:add(text)
end
end
end
-- Pre tags returned by a module are html tags, not like wikitext <pre>...</pre>.
return '<pre>\n' .. mw.text.nowiki(result:join('\n')) .. '\n</pre>'
end
local function _run_tests(frame, all_tests, args)
local function safe_cell(text, multiline)
-- For testing {{convert}}, want wikitext like '[[kilogram|kg]]' to be unchanged
-- so the link works and so the displayed text is short (just "kg" in example).
text = text:gsub('(%[%[[^%[%]]-)|(.-%]%])', '%1\0%2') -- replace pipe in piped link with a zero byte
text = text:gsub('{', '{'):gsub('|', '|') -- escape '{' and '|'
text = text:gsub('%z', '|') -- restore pipe in piped link
if multiline then
text = text:gsub('\\n', '<br />')
end
return text
end
local function nowiki_cell(text, multiline)
text = mw.text.nowiki(text)
if multiline then
text = text:gsub('\\n', '<br />')
end
return text
end
local stats = { pass = 0, fail = 0, ignored = 0, template = args.template }
local result = Collection.new()
result:add('{| class="wikitable sortable"')
result:add('! Template !! Expected !! Actual, if different !! Status')
for _, item in ipairs(all_tests) do
local template, expected = item[1], item[2] or ''
if template then
local actual = run_template(frame, template, args, true)
local sbox, actual, isfail = status_box(stats, expected, actual)
result:add('|-')
result:add('| ' .. safe_cell(template))
result:add('| ' .. safe_cell(expected, true))
result:add('| ' .. safe_cell(actual, true))
result:add('| ' .. sbox)
if isfail then
result:add('|-')
result:add('| align="center"| (above, nowiki)')
result:add('| ' .. nowiki_cell(normalize(expected), true))
result:add('| ' .. nowiki_cell(normalize(actual), true))
result:add('|')
end
else
local text = item.text
if text and text:sub(1, 3) == '---' then
result:add('|-')
result:add('| colspan="3" style="color:white;background:silver;" | ' .. safe_cell(strip(text:sub(4)), true))
result:add('| ' .. status_box(stats, '', '', true))
end
end
end
result:add('|}')
return status_text(stats) .. '\n\n' .. result:join('\n')
end
local function get_page_content(page_title, ignore_error)
local t = mw.title.new(page_title)
if t then
local content = t:getContent()
if content then
if content:sub(-1) ~= '\n' then
content = content .. '\n'
end
return content
end
end
if not ignore_error then
error('Could not read wikitext from "[[' .. page_title .. ']]".', 0)
end
end
local function _compare(frame, page_pairs)
local function diff_link(title1, title2)
return '<span class="plainlinks">[' ..
tostring(mw.uri.fullUrl('Special:ComparePages',
{ page1 = title1, page2 = title2 })) ..
' diff]</span>'
end
local function link(title)
return '[[' .. title .. ']]'
end
local function message(text, isgood)
local color = isgood and 'green' or 'darkred'
return '<span style="color:' .. color .. ';">' .. text .. '</span>'
end
local result = Collection.new()
for _, item in ipairs(page_pairs) do
local label
local title1 = item[1]
local title2 = item[2]
if title1 == title2 then
label = message('same title', false)
else
local content1 = get_page_content(title1, true)
local content2 = get_page_content(title2, true)
if not content1 or not content2 then
label = message('does not exist', false)
elseif content1 == content2 then
label = message('same content', true)
else
label = message('different', false) .. ' (' .. diff_link(title1, title2) .. ')'
end
end
result:add('*' .. link(title1) .. ' • ' .. link(title2) .. ' • ' .. label)
end
return result:join('\n')
end
local function sections(text)
return {
first = 1, -- just after the newline at the end of the last heading
this_section = 1,
next_heading = function(self)
local first = self.first
while first <= #text do
local last, heading
first, last, heading = text:find('==+[\t ]*([^\n]-)[\t ]*==+[\t\r ]*\n', first)
if first then
if first == 1 or text:sub(first - 1, first - 1) == '\n' then
self.this_section = first
self.first = last + 1
return heading
end
first = last + 1
else
break
end
end
self.first = #text + 1
return nil
end,
current_section = function(self)
local first = self.this_section
local last = text:find('\n==[^\n]-==[\t\r ]*\n', first)
if not last then
last = -1
end
return text:sub(first, last)
end,
}
end
local function get_tests(frame, tests)
local args = frame.args
local page_title, section_title = args.page, args.section
local show_all = (args.show == 'all')
if not empty(page_title) then
if not empty(tests) then
error('Invoke must not set "page=' .. page_title .. '" if also setting p.tests.', 0)
end
if page_title:sub(1, 2) == '[[' and page_title:sub(-2) == ']]' then
page_title = strip(page_title:sub(3, -3))
end
tests = get_page_content(page_title)
if not empty(section_title) then
local s = sections(tests)
while true do
local heading = s:next_heading()
if heading then
if heading == section_title then
tests = s:current_section()
break
end
else
error('Section "' .. section_title .. '" not found in page [[' .. page_title .. ']].', 0)
end
end
end
end
if type(tests) ~= 'string' then
if type(tests) == 'table' then
return tests
end
error('No tests were specified; see [[Module:Convert/tester/doc]].', 0)
end
if tests:sub(-1) ~= '\n' then
tests = tests .. '\n'
end
local template_count = 0
local all_tests = Collection.new()
for line in (tests):gmatch('([^\n]-)[\t\r ]*\n') do
local template, expected = line:match('^({{.-}})%s*(.-)%s*$')
if template then
template_count = template_count + 1
all_tests:add({ template, expected })
elseif show_all then
all_tests:add({ text = line })
end
end
if template_count == 0 then
error('No templates found; see [[Module:Convert/tester/doc]].', 0)
end
return all_tests
end
local function main(frame, p, worker)
local ok, result = pcall(get_tests, frame, p.tests)
if ok then
ok, result = pcall(worker, frame, result, frame.args)
if ok then
return result
end
end
return '<strong class="error">Error</strong>\n\n' .. result
end
local modules = {
-- For convenience, a key defined here can be used to refer to the
-- corresponding list of modules.
countries = { -- Commons
'Countries',
'Countries/Africa',
'Countries/Americas',
'Countries/Arab world',
'Countries/Asia',
'Countries/Caribbean',
'Countries/Central America',
'Countries/Europe',
'Countries/North America',
'Countries/North America (subcontinent)',
'Countries/Oceania',
'Countries/South America',
'Countries/United Kingdom',
},
convert = {
'Convert',
'Convert/data',
'Convert/text',
'Convert/extra',
'Convert/wikidata',
'Convert/wikidata/data',
},
cs1 = {
'Citation/CS1',
'Citation/CS1/Configuration',
},
cs1all = {
'Citation/CS1',
'Citation/CS1/Configuration',
'Citation/CS1/Whitelist',
'Citation/CS1/Date validation',
},
team = {
'Team appearances list',
'Team appearances list/data',
'Team appearances list/show',
},
val = {
'Val',
'Val/units',
},
}
local p = {}
function p.compare(frame)
local page_pairs = p.pairs
if not page_pairs then
local args = frame.args
if not args[2] then
local builtins = modules[args[1] or 'convert']
if builtins then
args = builtins
end
end
page_pairs = {}
for i, title in ipairs(args) do
if not title:find(':', 1, true) then
title = 'Module:' .. title
end
page_pairs[i] = { title, title .. '/sandbox' }
end
end
local ok, result = pcall(_compare, frame, page_pairs)
if ok then
return result
end
return '<strong class="error">Error</strong>\n\n' .. result
end
p.check_sandbox = p.compare
function p.make_tests(frame)
return main(frame, p, _make_tests)
end
function p.run_tests(frame)
return main(frame, p, _run_tests)
end
return p
Content Disclaimer
Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.
- The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
- There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
- It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
- Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
- Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.