Module:UnitTests/sandbox

-- UnitTester provides unit testing for other Lua scripts. For details see Wikipedia:Lua#Unit_testing.

-- For user documentation see talk page.

local UnitTester = {}

local frame, tick, cross, should_highlight

local result_table_header = "

class=\"wikitable unit-tests-result\"\n|+ %s\n! !! Text !! Expected !! Actual"

local result_table_live_sandbox_header = "{|class=\"wikitable unit-tests-result\"\n|+ %s\n! !! Test !! Live !! Sandbox !! Expected"

local result_table = { n = 0 }

local result_table_mt = {

insert = function (self, ...)

local n = self.n

for i = 1, select('#', ...) do

local val = select(i, ...)

if val ~= nil then

n = n + 1

self[n] = val

end

end

self.n = n

end,

insert_format = function (self, ...)

self:insert(string.format(...))

end,

concat = table.concat

}

result_table_mt.__index = result_table_mt

setmetatable(result_table, result_table_mt)

local num_failures = 0

local num_runs = 0

local function first_difference(s1, s2)

s1, s2 = tostring(s1), tostring(s2)

if s1 == s2 then return '' end

local max = math.min(#s1, #s2)

for i = 1, max do

if s1:sub(i,i) ~= s2:sub(i,i) then return i end

end

return max + 1

end

local function return_varargs(...)

return ...

end

function UnitTester:calculate_output(text, expected, actual, options)

-- Set up some variables for throughout for ease

num_runs = num_runs + 1

local options = options or {}

-- Fix any stripmarkers if asked to do so to prevent incorrect fails

local compared_expected = expected

local compared_actual = actual

if options.templatestyles then

local pattern = '(\127[^\127]*UNIQ%-%-templatestyles%-)(%x+)(%-QINU[^\127]*\127)'

local _, expected_stripmarker_id = compared_expected:match(pattern) -- when module rendering has templatestyles strip markers, use ID from expected to prevent false test fail

if expected_stripmarker_id then

compared_actual = compared_actual:gsub(pattern, '%1' .. expected_stripmarker_id .. '%3') -- replace actual id with expected id; ignore second capture in pattern

compared_expected = compared_expected:gsub(pattern, '%1' .. expected_stripmarker_id .. '%3') -- account for other strip markers

end

end

if options.stripmarker then

local pattern = '(\127[^\127]*UNIQ%-%-%l+%-)(%x+)(%-%-?QINU[^\127]*\127)'

local _, expected_stripmarker_id = compared_expected:match(pattern)

if expected_stripmarker_id then

compared_actual = compared_actual:gsub(pattern, '%1' .. expected_stripmarker_id .. '%3')

compared_expected = compared_expected:gsub(pattern, '%1' .. expected_stripmarker_id .. '%3')

end

end

-- Perform the comparison

local success = compared_actual == compared_expected

if not success then

num_failures = num_failures + 1

end

-- Sort the wikitext for displaying the results

if options.combined then

-- We need 2 rows available for the expected and actual columns

-- Top one is parsed, bottom is unparsed

local differs_at = self.differs_at and (' \n| rowspan=2|' .. first_difference(compared_expected, compared_actual)) or ''

-- Local copies of tick/cross to allow for highlighting

local highlight = (should_highlight and not success and 'style="background:#fc0;" ') or ''

result_table:insert( -- Start output

'| ', highlight, 'rowspan=2|', success and tick or cross, -- Tick/Cross (2 rows)

' \n| rowspan=2|', mw.text.nowiki(text), ' \n| ', -- Text used for the test (2 rows)

expected, ' \n| ', actual, -- The parsed outputs (in the 1st row)

differs_at, ' \n

\n| ', -- Where any relevant difference was (2 rows)

mw.text.nowiki(expected), ' \n| ', mw.text.nowiki(actual), -- The unparsed outputs (in the 2nd row)

'\n

\n' -- End output

)

else

-- Display normally with whichever option was preferred (nowiki/parsed)

local differs_at = self.differs_at and (' \n| ' .. first_difference(compared_expected, compared_actual)) or ''

local formatting = options.nowiki and mw.text.nowiki or return_varargs

local highlight = (should_highlight and not success and 'style="background:#fc0;"|') or ''

result_table:insert( -- Start output

'| ', highlight, success and tick or cross, -- Tick/Cross

' \n| ', mw.text.nowiki(text), ' \n| ', -- Text used for the test

formatting(expected), ' \n| ', formatting(actual), -- The formatted outputs

differs_at, -- Where any relevant difference was

'\n

\n' -- End output

)

end

end

function UnitTester:preprocess_equals(text, expected, options)

local actual = frame:preprocess(text)

self:calculate_output(text, expected, actual, options)

end

function UnitTester:preprocess_equals_many(prefix, suffix, cases, options)

for _, case in ipairs(cases) do

self:preprocess_equals(prefix .. case[1] .. suffix, case[2], options)

end

end

function UnitTester:preprocess_equals_preprocess(text1, text2, options)

local actual = frame:preprocess(text1)

local expected = frame:preprocess(text2)

self:calculate_output(text1, expected, actual, options)

end

function UnitTester:preprocess_equals_compare(live, sandbox, expected, options)

local live_text = frame:preprocess(live)

local sandbox_text = frame:preprocess(sandbox)

local highlight_live = false

local highlight_sandbox = false

num_runs = num_runs + 1

if live_text == expected and sandbox_text == expected then

result_table:insert('| ', tick)

else

result_table:insert('| ', cross)

num_failures = num_failures + 1

if live_text ~= expected then

highlight_live = true

end

if sandbox_text ~= expected then

highlight_sandbox = true

end

end

local formatting = (options and options.nowiki and mw.text.nowiki) or return_varargs

local differs_at = self.differs_at and (' \n| ' .. first_difference(expected, live_text) or first_difference(expected, sandbox_text)) or ''

result_table:insert(

' \n| ',

mw.text.nowiki(live),

should_highlight and highlight_live and ' \n|style="background: #fc0;"| ' or ' \n| ',

formatting(live_text),

should_highlight and highlight_sandbox and ' \n|style="background: #fc0;"| ' or ' \n| ',

formatting(sandbox_text),

' \n| ',

formatting(expected),

differs_at,

"\n

\n"

)

end

function UnitTester:preprocess_equals_preprocess_many(prefix1, suffix1, prefix2, suffix2, cases, options)

for _, case in ipairs(cases) do

self:preprocess_equals_preprocess(prefix1 .. case[1] .. suffix1, prefix2 .. (case[2] and case[2] or case[1]) .. suffix2, options)

end

end

function UnitTester:preprocess_equals_sandbox_many(module, function_name, cases, options)

for _, case in ipairs(cases) do

local live = module .. "|" .. function_name .. "|" .. case[1] .. "}}"

local sandbox = module .. "/sandbox|" .. function_name .. "|" .. case[1] .. "}}"

self:preprocess_equals_compare(live, sandbox, case[2], options)

end

end

function UnitTester:equals(name, actual, expected, options)

num_runs = num_runs + 1

if actual == expected then

result_table:insert('| ', tick)

else

result_table:insert('| ', cross)

num_failures = num_failures + 1

end

local formatting = (options and options.nowiki and mw.text.nowiki) or return_varargs

local differs_at = self.differs_at and (' \n| ' .. first_difference(expected, actual)) or ''

local display = options and options.display or return_varargs

result_table:insert(' \n| ', name, ' \n| ',

formatting(tostring(display(expected))), ' \n| ',

formatting(tostring(display(actual))), differs_at, "\n

\n")

end

local function deep_compare(t1, t2, ignore_mt)

local ty1 = type(t1)

local ty2 = type(t2)

if ty1 ~= ty2 then return false end

if ty1 ~= 'table' and ty2 ~= 'table' then return t1 == t2 end

local mt = getmetatable(t1)

if not ignore_mt and mt and mt.__eq then return t1 == t2 end

for k1, v1 in pairs(t1) do

local v2 = t2[k1]

if v2 == nil or not deep_compare(v1, v2) then return false end

end

for k2, v2 in pairs(t2) do

local v1 = t1[k2]

if v1 == nil or not deep_compare(v1, v2) then return false end

end

return true

end

local function val_to_str(obj)

local function table_key_to_str(k)

if type(k) == 'string' and mw.ustring.match(k, '^[_%a][_%a%d]*$') then

return k

else

return '[' .. val_to_str(k) .. ']'

end

end

if type(obj) == "string" then

obj = mw.ustring.gsub(obj, "\n", "\\n")

if mw.ustring.match(mw.ustring.gsub(obj, '[^\'"]', ''), '^"+$') then

return "'" .. obj .. "'"

end

return '"' .. mw.ustring.gsub(obj, '"', '\\"' ) .. '"'

elseif type(obj) == "table" then

local result, checked = {}, {}

for k, v in ipairs(obj) do

table.insert(result, val_to_str(v))

checked[k] = true

end

for k, v in pairs(obj) do

if not checked[k] then

table.insert(result, table_key_to_str(k) .. '=' .. val_to_str(v))

end

end

return '{' .. table.concat(result, ',') .. '}'

else

return tostring(obj)

end

end

function UnitTester:equals_deep(name, actual, expected, options)

num_runs = num_runs + 1

if deep_compare(actual, expected) then

result_table:insert('| ', tick)

else

result_table:insert('| ', cross)

num_failures = num_failures + 1

end

local formatting = (options and options.nowiki and mw.text.nowiki) or return_varargs

local actual_str = val_to_str(actual)

local expected_str = val_to_str(expected)

local differs_at = self.differs_at and (' \n| ' .. first_difference(expected_str, actual_str)) or ''

result_table:insert(' \n| ', name, ' \n| ', formatting(expected_str),

' \n| ', formatting(actual_str), differs_at, "\n

\n")

end

function UnitTester:iterate(examples, func)

require 'libraryUtil'.checkType('iterate', 1, examples, 'table')

if type(func) == 'string' then

func = self[func]

elseif type(func) ~= 'function' then

error(("bad argument #2 to 'iterate' (expected function or string, got %s)")

:format(type(func)), 2)

end

for i, example in ipairs(examples) do

if type(example) == 'table' then

func(self, unpack(example))

elseif type(example) == 'string' then

self:heading(example)

else

error(('bad example #%d (expected table, got %s)')

:format(i, type(example)), 2)

end

end

end

function UnitTester:heading(text)

result_table:insert_format(' ! colspan="%u" style="text-align: left" | %s \n

\n ',

self.columns, text)

end

function UnitTester:run(frame_arg)

frame = frame_arg

self.frame = frame

self.differs_at = frame.args['differs_at']

tick = frame:preprocess('{{Tick}}')

cross = frame:preprocess('{{Cross}}')

local table_header = result_table_header

if frame.args['live_sandbox'] then

table_header = result_table_live_sandbox_header

end

if frame.args.highlight then

should_highlight = true

end

self.columns = 4

if self.differs_at then

table_header = table_header .. ' !! Differs at'

self.columns = self.columns + 1

end

-- Sort results into alphabetical order.

local self_sorted = {}

for key, _ in pairs(self) do

if key:find('^test') then

table.insert(self_sorted, key)

end

end

table.sort(self_sorted)

-- Add results to the results table.

for _, value in ipairs(self_sorted) do

result_table:insert_format("

%s

\n", value)

result_table:insert_format(table_header .. "\n

\n", value)

self[value](self)

result_table:insert("

\n")

end

return (num_runs == 0 and "No tests were run."

or num_failures == 0 and "All " .. num_runs .. " tests passed."

or "" .. num_failures .. " of " .. num_runs .. " tests failed.Category:Failed Lua testcases using Module:UnitTests"

) .. "\n\n" .. frame:preprocess(result_table:concat())

end

function UnitTester:new()

local o = {}

setmetatable(o, self)

self.__index = self

return o

end

local p = UnitTester:new()

function p.run_tests(frame) return p:run(frame) end

return p