Módulo:Convert/tester

Ourige: Biquipédia, la anciclopédia lhibre.

La decumentaçon pa este módulo puode ser criada na páigina Módulo:Convert/tester/doc

-- 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)
        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
    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
    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, forcename, 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 '(modelo ambálido)'
    end
    local args = {}
    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
        args[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 = forcename or strip(field)
            if templatename == '' then
                return '(modelo ambálido)'
            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 args[index] ~= nil do
                    -- Skip any explicit numbered parameters like "|5=five".
                    index = index + 1
                end
                put_arg(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 = 'Erro: ' .. 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 <pre>...</pre>.
    return '<pre>\n' .. mw.text.nowiki(result:join('\n')) .. '\n</pre>'
end

local function _run_tests(frame, all_tests, forcename)
    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] or ''
        if template then
            local actual = run_template(frame, template, forcename, 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(expected, true))
                result:add('| ' .. nowiki_cell(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)
    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('Nun cunseguiu lher l biquitesto de "[[' .. page_title .. ']]".', 0)
end

local function _compare(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
        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('Seçon "' .. section_title .. '" nun fui ancontrada na páigina [[' .. page_title .. ']].', 0)
                end
            end
        end
    end
    if type(tests) ~= 'string' then
        if type(tests) == 'table' then
            return tests
        end
        error('Sin testes defenidos; ber [[Módulo: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('^({{.-}})%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('Sin modelos ancontrados; ber [[Módulo: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 '<strong class="error">Erro</strong>\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',
        'Convert/wikidata',
        'Convert/wikidata/data',
    },
    cs1 = {
        'Citaçon/CS1',
        'Citaçon/CS1/Cunfiguraçon',
    },
    cs1all = {
        'Citaçon/CS1',
        'Citaçon/CS1/Cunfiguraçon',
        'Citaçon/CS1/Whitelist',
        'Citaçon/CS1/Balidaçon datas',
    },
    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 = 'Módulo:' .. title
            end
            page_pairs[i] = { title, title .. '/Testes' }
        end
    end
    local ok, result = pcall(_compare, frame, page_pairs)
    if ok then
        return result
    end
    return '<strong class="error">Erro</strong>\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