Module:Convert/tester

From Lojban
Jump to navigation Jump to search
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

-- 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 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, 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 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 '' .. msg .. ignored_text .. '.' end

local function run_template(frame, template, forcename, collapse_multiline) -- Template "" -- gives args { " abc ", "def", name = "ghi jkl" }. if template:sub(1, 2) == 'Template:' 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 = forcename 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 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, forcename) 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() for _, item in ipairs(all_tests) do local template = item[1] if template then local actual = run_template(frame, template, forcename, 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, forcename) local function safe_cell(text, multiline) -- For testing [convert: needs a number], 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 } 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] or if template then local actual = run_template(frame, template, forcename, 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 else local text = item.text if text and text:sub(1, 3) == '---' then actual, sbox, isfail = status_box(stats, , , true) result:add('|-') result:add('| colspan="3" style="color:white;background:silver;" | ' .. safe_cell(strip(text:sub(4)), true)) result:add('| ' .. sbox) end 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 if content:sub(-1) ~= '\n' then content = content .. '\n' end return content end end error('Could not read wikitext from "' .. page_title .. '".', 0) end

local function _compare(frame, page_pairs) 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() 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 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() for line in (tests):gmatch('([^\n]-)[\t\r ]*\n') do local template, expected = line:match('^(Template:.-)%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 args = frame.args local ok, result = pcall(get_tests, frame, p.tests) if ok then ok, result = pcall(worker, frame, result, args.template) 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. convert = { 'Convert', 'Convert/data', 'Convert/text', 'Convert/extra', }, cs1 = { 'Citation/CS1', 'Citation/CS1/Configuration', }, cs1all = { 'Citation/CS1', 'Citation/CS1/Configuration', 'Citation/CS1/Whitelist', 'Citation/CS1/Date validation', }, }

local p = {}

function p.compare(frame) local pairs = p.pairs if not 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 pairs = {} for i, title in ipairs(args) do if not title:find(':', 1, true) then title = 'Module:' .. title end pairs[i] = { title, title .. '/sandbox' } end end local ok, result = pcall(_compare, frame, 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