-- 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 = ''
label = 'Cmnt'
elseif expected == '' then
stats.ignored = stats.ignored + 1
return '', actual
elseif normalize(expected) == normalize(actual) then
stats.pass = stats.pass + 1
actual = ''
label = 'Pass'
else
stats.fail = stats.fail + 1
label = 'Fail'
isfail = true
end
local sbox = ' | ' .. 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
msg = 'No tests performed'
else
msg = string.format('All %d tests passed', stats.pass)
end
else
msg = string.format('%d test%s failed', stats.fail, stats.fail == 1 and '' or 's')
end
if stats.ignored == 0 then
ignored_text = ''
else
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>' ..
msg .. ignored_text .. '.</span>'
end  
local function run_template(, template, args, collapse_multiline)
-- Template ""
-- gives xargs { " abc ", "def", name = "ghi jkl" }.
if template:sub(1, 2) == '' 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 :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(, 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(, 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(, all_tests, args)
local function safe_cell(text, multiline)
-- For testing , want wikitext like '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('{', '&#123;'):gsub('|', '&#124;') -- escape '{' and '|'
text = text:gsub('%z', '|') -- restore pipe in piped link
if multiline then
text = text:gsub('\\n', '
')
end
return text
end
local function nowiki_cell(text, multiline)
text = mw.text.nowiki(text)
if multiline then
text = text:gsub('\\n', '
')
end
return text
end
local stats = { pass = 0, fail = 0, ignored = 0, template = args.template }
local result = Collection.new
result:add('{|')
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(, 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('|| (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('| | ' .. 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(, page_pairs)
local prefix = .args.prefix or '*'
local function diff_link(title1, title2)
return '<span>[' ..
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>' .. 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