Module:Convert/tester: Difference between revisions

Content deleted Content added
have to use pcall to get args
fix typo in regex reported by DePiep
 
(28 intermediate revisions by 4 users not shown)
Line 1:
-- Test the output from a template thatby invokescomparing [[Module:Convert]]it with fixed text.
-- The expected text must be in a single line, but can include
-- (but can be used to test the output from any template).
-- "\n" (two characters) to indicate that a newline is expected.
-- The result is compared with fixed text.
-- Tests are run (or created) by setting p.tests (string or table), or
-- The expected text must be in a single line, but can include "\n" (two characters)
-- by setting page=PAGE_TITLE (and optionally section=SECTION_TITLE),
-- to indicate that a newline is expected.
-- Tests are run by setting p.tests then executing run_tests, (or make_tests).
-- by setting page=PAGE_TITLE (and optionally section=SECTION_TITLE) in the invoke,
-- then executing run_tests.
-- Adapted from [[Module:ConvertTestcase]].
 
local functionCollection collection()= {}
Collection.__index = Collection
-- Return a table to hold lines of text.
do
return {
function Collection:add(item)
n = 0,
if item ~= nil then
add = function (self, s)
self.n = self.n + 1
self[self.n] = sitem
end,
end
join = function (self, sep)
function Collection:join(sep)
return table.concat(self, sep or '\n')
return table.concat(self, sep)
end,
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 status_boxnormalize(stats, expected, actualtext)
-- Return text with any strip markers normalized by replacing the
local label, bgcolor, align
-- unique number with a fixed value so comparisons work.
if expected == '' then
return text:gsub('(\127[^\127]*UNIQ[^\127]*%-)(%x+)(-QINU[^\127]*\127)', '%100000000%3')
stats.ignored = stats.ignored + 1
end
return actual, ''
 
elseif expected == actual then
local function status_box(stats, expected, actual, iscomment)
stats.pass = stats.pass + 1
local label, bgcolor, align, isfail
actual = ''
if iscomment then
align = 'center'
actual = ''
bgcolor = 'green'
label align = 'Passcenter'
bgcolor = 'silver'
else
label = 'Cmnt'
stats.fail = stats.fail + 1
elseif expected == '' then
align = 'center'
stats.ignored = stats.ignored + 1
bgcolor = 'red'
return '', actual
label = 'Fail'
elseif normalize(expected) == normalize(actual) then
end
stats.pass = stats.pass + 1
return actual, 'style="text-align:' .. align .. ';color:white;background:' .. bgcolor .. ';" | ' .. label
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.fail == 0template then
ttext = "'''Using [[Template:" .. stats.template .. "]]:''' "
if stats.pass == 0 then
else
bgcolor = 'salmon'
ttext = ''
msg = 'No tests performed'
end
else
if stats.fail == 0 then
bgcolor = 'green'
if stats.pass == 0 then
msg = string.format('All %d tests passed', stats.pass)
bgcolor = 'salmon'
end
msg = 'No tests performed'
else
else
bgcolor = 'darkred'
bgcolor = 'green'
msg = string.format('%d test%s failed', stats.fail, stats.fail == 1 and '' or 's')
msg = string.format('All %d tests passed', stats.pass)
end
end
if stats.ignored == 0 then
else
ignored_text = ''
bgcolor = 'darkred'
else
msg = string.format('%d test%s failed', stats.fail, stats.fail == 1 and '' or 's')
bgcolor = 'salmon'
end
ignored_text = string.format(', %d test%s ignored because expected text is blank', stats.ignored, stats.ignored == 1 and '' or 's')
if stats.ignored == 0 then
end
ignored_text = ''
return '<span style="font-size:120%;color:white;background-color:' .. bgcolor .. ';">' .. msg .. ignored_text .. '.</span>'
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 }}"
local title, argstr = template:match('^{{%s*(.-)%s*|(.*)}}$')
-- gives xargs { " abc ", "def", name = "ghi jkl" }.
if title == nil or title == '' or argstr == '' then
if template:sub(1, 2) == '{{' and template:sub(-2, -1) == '}}' then
return '(invalid template)'
template = template:sub(3, -3) .. '|' -- append sentinel to get last field
end
else
local args = {}
return '(invalid template)'
for item in string.gmatch(argstr .. '|', '(.-)|') do
end
local p = item:find('=', 1, true)
local xargs = {}
if p then
local index = 1
args[item:sub(1, p-1)] = item:sub(p+1)
local templatename
else
local function put_arg(k, v)
table.insert(args, item)
-- Kludge: Module:Val uses Module:Arguments which trims arguments and
end
-- omits blank arguments. Simulate that here.
end
-- LATER Need a parameter to control this.
return frame:expandTemplate({ title = title, args = args })
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 _run_tests_make_tests(frame, all_tests, args)
local maxlen = 38
if type(all_tests) ~= 'string' then
for _, item in ipairs(all_tests) do
error('No tests were specified; see [[Module:Convert/tester/doc]].', 0)
local template = item[1]
end
if template then
local function collapse_multiline(text)
local templen = mw.ustring.len(template)
return text:gsub('\n', '\\n')
item.templen = templen
end
if maxlen < templen and templen <= 70 then
local function safe_cell(text, multiline)
maxlen = templen
-- For testing {{convert}}, want wikitext like '[[kilogram|kg]]' to be unchanged
end
-- so the link works and so the displayed text is short (just "kg" in example).
end
text = text:gsub('(%[%[[^%[%]]-)|(.-%]%])', '%1\0%2') -- replace pipe in piped link with a zero byte
end
text = text:gsub('{', '&#123;'):gsub('|', '&#124;') -- escape '{' and '|'
local result = Collection.new()
text = text:gsub('%z', '|') -- restore pipe in piped link
for _, item in ipairs(all_tests) do
if multiline then
local template = item[1]
text = text:gsub('\\n', '<br />')
if template then
end
local actual = run_template(frame, template, args, true)
return text
local pad = string.rep(' ', maxlen - item.templen) .. ' '
end
result:add(template .. pad .. actual)
local stats = { pass = 0, fail = 0, ignored = 0 }
else
local result = collection()
local text = item.text
result:add('{| class="wikitable"')
if text then
result:add('! Template !! Expected !! Actual, if different !! Status')
result:add(text)
for template, expected in all_tests:gmatch('({{.-}})(.-)\n') do
end
expected = strip(expected)
end
local actual = collapse_multiline(run_template(frame, template))
end
local actual, sbox = status_box(stats, expected, actual)
-- Pre tags returned by a module are html tags, not like wikitext <pre>...</pre>.
result:add('|-')
return '<pre>\n' .. mw.text.nowiki(result:addjoin('| \n')) .. safe_cell(template))'\n</pre>'
result:add('| ' .. safe_cell(expected, true))
result:add('| ' .. safe_cell(actual, true))
result:add('| ' .. sbox)
end
result:add('|}')
return status_text(stats) .. '\n\n' .. result:join()
end
 
local function get_page_content_run_tests(page_titleframe, all_tests, args)
local function safe_cell(text, multiline)
local t = mw.title.new(page_title)
-- For testing {{convert}}, want wikitext like '[[kilogram|kg]]' to be unchanged
if t then
-- so the link works and so the displayed text is short (just "kg" in example).
local content = t:getContent()
text = text:gsub('(%[%[[^%[%]]-)|(.-%]%])', '%1\0%2') -- replace pipe in piped link with a zero byte
if content then
text = text:gsub('{', '&#123;'):gsub('|', '&#124;') -- escape '{' and '|'
return content
text = text:gsub('%z', '|') -- restore pipe in piped link
end
if multiline then
end
text = text:gsub('\\n', '<br />')
error('Could not read wikitext from [["' .. page_title .. ']]".', 0)
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 prefix = frame.args.prefix or '*'
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(prefix .. 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)
next_heading = function(self)
local first = self.first
while local first <= #text doself.first
while first <= #text do
local last, heading
local last, heading
first, last, heading = text:find('==+ *([^\n]-) *==+ *\n', first)
first, last, heading = text:find('==+[\t ]*([^\n]-)[\t ]*==+[\t\r ]*\n', first)
if first then
if first then
if first == 1 or text:sub(first-1, first-1) == '\n' then
if first == 1 or text:sub(first - 1, first - 1) == '\n' then
self.first = last + 1
self.this_section = first
return heading
self.first = last + 1
end
return heading
first = last + 1
end
else
first = last + 1
break
else
end
break
end
end
self.first = #text + 1
end
return nil
self.first = #text + 1
end,
return nil
current_body = function(self)
end,
local first = self.first
current_section = function(self)
local last = text:find('\n==[^\n]-== *\n', first)
local first = self.this_section
if last then
local last = text:find('\n==[^\n]-==[\t\r ]*\n', first)
return text:sub(first, last)
if not last then
end
last = -1
return text:sub(first)
end,
return text:sub(first, last)
}
end,
}
end
 
local function get_tests(page_titleframe, section_title, test_texttests)
local args = frame.args
if not empty(page_title) then
local page_title, section_title = args.page, args.section
if not empty(test_text) then
local show_all = (args.show == 'all')
error('Invoke must not set "page=' .. page_title .. '" if also setting p.tests.', 0)
if not empty(page_title) then
end
if not empty(tests) then
test_text = get_page_content(page_title)
error('Invoke must not set "page=' .. page_title .. '" if also setting p.tests.', 0)
if not empty(section_title) then
end
local s = sections(test_text)
if page_title:sub(1, 2) == '[[' and page_title:sub(-2) == ']]' then
while true do
page_title = strip(page_title:sub(3, -3))
local heading = s:next_heading()
end
if heading then
tests = get_page_content(page_title)
if heading == section_title then
if not empty(section_title) then
return s:current_body()
local s = sections(tests)
end
while true do
else
local heading = s:next_heading()
error('Section "' .. section_title .. '" not found in page [[' .. page_title .. ']].', 0)
if heading then
end
if heading == section_title then
end
tests = s:current_section()
end
break
end
end
return test_text
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)
local args = frame.args
local ok, result = pcall(get_tests, args.page, args.section, p.tests)
if ok then
ok, result = pcall(_run_tests, frame, result)
if ok then
return result
end
end
return "'''Error:'''\n\n" .. result
end