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 10:15, 11 October 2013 (try "make_expected=yes" option). 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 by setting p.tests then executing run_tests, or
-- by setting page=PAGE_TITLE (and optionally section=SECTION_TITLE) in the invoke,
-- then executing run_tests.
-- Adapted from [[Module:ConvertTestcase]].

local function boolean(text)
    -- Return true if text represents a "true" option value.
    if text then
        text = text:lower()
        if text == 'on' or text == 'yes' then
            return true
        end
    end
    return false
end

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)
    local title, argstr = template:match('^{{%s*(.-)%s*|(.*)}}$')
    if title == nil or title == '' or argstr == '' then
        return '(invalid template)'
    end
    local args = {}
    for item in string.gmatch(argstr .. '|', '(.-)|') do
        local p = item:find('=', 1, true)
        if p then
            args[item:sub(1, p-1)] = item:sub(p+1)
        else
            table.insert(args, item)
        end
    end
    local function expand(t)
        return frame:expandTemplate(t)
    end
    local ok, result = pcall(expand, { title = title, args = args })
    if ok then
        return result
    end
    return 'Error: ' .. result
end

local function _run_tests(frame, all_tests)
    if type(all_tests) ~= 'string' then
        error('No tests were specified; see [[Module:Convert/tester/doc]].', 0)
    end
    local make_expected = boolean(frame.args.make_expected)
    local function collapse_multiline(text)
        return text:gsub('\n', '\\n')
    end
    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 maxlen = 60  -- should determine this by examining each template
    local stats = { pass = 0, fail = 0, ignored = 0 }
    local result = collection()
    if not make_expected then
        result:add('{| class="wikitable"')
        result:add('! Template !! Expected !! Actual, if different !! Status')
    end
    for pos, template, expected in all_tests:gmatch('()({{.-}})(.-)\n') do
        -- Skip templates that are not at the start of a line to reduce likelihood
        -- of using unwanted templates when reading a page.
        if pos == 1 or all_tests:sub(pos-1, pos-1) == '\n' then
            local actual = collapse_multiline(run_template(frame, template))
            if make_expected then
                local pad = string.rep(' ', maxlen - mw.ustring.len(template))
                result:add(template .. pad .. ' ' .. actual)
            else
                local sbox, isfail
                expected = strip(expected)
                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
        end
    end
    if make_expected then
        -- 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
    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 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(page_title, section_title, test_text)
    if not empty(page_title) then
        if not empty(test_text) 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
        test_text = get_page_content(page_title)
        if not empty(section_title) then
            local s = sections(test_text)
            while true do
                local heading = s:next_heading()
                if heading then
                    if heading == section_title then
                        return s:current_body()
                    end
                else
                    error('Section "' .. section_title .. '" not found in page [[' .. page_title .. ']].', 0)
                end
            end
        end
    end
    return test_text
end

local p = {}

function p.run_tests(frame)
    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 '<strong class="error">Error</strong>\n\n' .. result
end

return p