Jump to content

Module:Convert/tester

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Johnuniq (talk | contribs) at 03:40, 6 November 2013 (new style: 4-column tabs). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

-- Test the output from a template that invokes [[Module:Convert]]
-- (but can be used to test the output from any template).
-- The result is compared 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).
-- Adapted from [[Module:ConvertTestcase]].

local function collection()
	-- Return a table to hold lines of text.
	return {
		n = 0,
		add = function (self, s)
			self.n = self.n + 1
			self[self.n] = s
		end,
		join = function (self, sep)
			return table.concat(self, sep or '\n')
		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_box(stats, expected, actual)
	local label, bgcolor, align, isfail
	if expected == '' then
		stats.ignored = stats.ignored + 1
		return actual, ''
	elseif expected == 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
	return actual, 'style="text-align:' .. align .. ';color:white;background:' .. bgcolor .. ';" | ' .. label, isfail
end

local function status_text(stats)
	local bgcolor, ignored_text, msg
	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 '<span style="font-size:120%;color:white;background-color:' .. bgcolor .. ';">' .. msg .. ignored_text .. '.</span>'
end

local function run_template(frame, template, collapse_multiline)
	-- Template "{{ example |  2  =  def  |  abc  |  name  =  ghi jkl  }}"
	-- gives args { "  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 args = {}
	local index = 1
	local templatename
	for field in template:gmatch('(.-)|') do
		if templatename == nil then
			templatename = 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
					args[i] = v
				else
					args[k] = v
				end
			else
				while args[index] ~= nil do
					-- Skip any explicit numbered parameters like "|5=five".
					index = index + 1
				end
				args[index] = field
			end
		end
	end
	local function expand(t)
		return frame:expandTemplate(t)
	end
	local ok, result = pcall(expand, { title = templatename, args = args })
	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)
	local maxlen = 38
	for _, item in ipairs(all_tests) do
		local template = item[1]
		local templen = mw.ustring.len(template)
		item.templen = templen
		if maxlen < templen and templen <= 70 then
			maxlen = templen
		end
	end
	local result = collection()
	for _, item in ipairs(all_tests) do
		local template = item[1]
		local actual = run_template(frame, template, true)
		local pad = string.rep(' ', maxlen - item.templen) .. '  '
		result:add(template .. pad .. actual)
	end
	-- Pre tags returned by a module are html tags, not like wikitext <pre>...</pre>.
	return '<pre>\n' .. mw.text.nowiki(result:join()) .. '\n</pre>\n'
end

local function _run_tests(frame, all_tests)
	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('{', '&#123;'):gsub('|', '&#124;')    -- 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 }
	local result = collection()
	result:add('{| class="wikitable"')
	result:add('! Template !! Expected !! Actual, if different !! Status')
	for _, item in ipairs(all_tests) do
		local template, expected = item[1], item[2]
		local actual = run_template(frame, template, true)
		local sbox, isfail
		actual, sbox, 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(expected, true))
			result:add('| ' .. nowiki_cell(actual, true))
			result:add('|')
		end
	end
	result:add('|}')
	return status_text(stats) .. '\n\n' .. result:join()
end

local function get_page_content(page_title)
	local t = mw.title.new(page_title)
	if t then
		local content = t:getContent()
		if content then
			return content
		end
	end
	error('Could not read wikitext from "[[' .. page_title .. ']]".', 0)
end

local function _check_sandbox(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()
	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)
			local content2 = get_page_content(title2)
			if 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
		next_heading = function(self)
			local first = self.first
			while first <= #text do
				local last, heading
				first, last, heading = text:find('==+ *([^\n]-) *==+ *\n', first)
				if first then
					if first == 1 or text:sub(first-1, first-1) == '\n' then
						self.first = last + 1
						return heading
					end
					first = last + 1
				else
					break
				end
			end
			self.first = #text + 1
			return nil
		end,
		current_body = function(self)
			local first = self.first
			local last = text:find('\n==[^\n]-== *\n', first)
			if last then
				return text:sub(first, last)
			end
			return text:sub(first)
		end,
	}
end

local function get_tests(frame, tests)
	local args = frame.args
	local page_title, section_title = args.page, args.section
	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_body()
						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
	local all_tests = collection()
	for line in (tests .. '\n'):gmatch('([^\n]+)\n') do
		local template, expected = line:match('^({{.-}})%s*(.-)%s*$')
		if template then
			all_tests:add({ template, expected })
		end
	end
	if all_tests.n == 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)
		if ok then
			return result
		end
	end
	return '<strong class="error">Error</strong>\n\n' .. result
end

local convert_pairs = {
	{ 'Module:Convert',       'Module:Convert/sandbox' },
	{ 'Module:Convert/data',  'Module:Convert/data/sandbox' },
	{ 'Module:Convert/text',  'Module:Convert/text/sandbox' },
	{ 'Module:Convert/extra', 'Module:Convert/extra/sandbox' },
}
local p = {}

function p.check_sandbox(frame)
	local ok, result = pcall(_check_sandbox, frame, p.pairs or convert_pairs)
	if ok then
		return result
	end
	return '<strong class="error">Error</strong>\n\n' .. result
end

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