-- 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 .. '' .. 		msg .. ignored_text .. '.' end  local function run_template(frame, 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 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 .... 	return '\n' .. mw.text.nowiki(result:join('\n')) .. '\n' end  local function _run_tests(frame, 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('{', '{'):gsub('|', '|')    -- 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('{| 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 '[' .. 			tostring(mw.uri.fullUrl('Special:ComparePages', 				{ page1 = title1, page2 = title2 })) .. 			' diff]' 	end 	local function link(title) 		return '' .. title .. '' 	end 	local function message(text, isgood) 		local color = isgood and 'green' or 'darkred' 		return '' .. text .. '' 	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) 			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 'Error\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 .. '/খেলাঘর' } 		end 	end 	local ok, result = pcall(_compare, frame, page_pairs) 	if ok then 		return result 	end 	return 'Error\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