# Module:Template test case

--[[  
A module for generating test case templates.  

This module incorporates code from the English Wikipedia's "Testcase table"  
module,[1] written by Frietjes [2] with contributions by Mr. Stradivarius [3]  
and Jackmcbarn,[4] and the English Wikipedia's "Testcase rows" module,[5]  
written by Mr. Stradivarius.  

The "Testcase table" and "Testcase rows" modules are released under the  
CC BY-SA 3.0 License [6] and the GFDL.[7]  

License: CC BY-SA 3.0 and the GFDL  
Author: Mr. Stradivarius  

[1] https://en.wikipedia.org/wiki/Module:Testcase_table  
[2] https://en.wikipedia.org/wiki/User:Frietjes  
[3] https://en.wikipedia.org/wiki/User:Mr._Stradivarius  
[4] https://en.wikipedia.org/wiki/User:Jackmcbarn  
[5] https://en.wikipedia.org/wiki/Module:Testcase_rows  
[6] https://en.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License  
[7] https://en.wikipedia.org/wiki/Wikipedia:Text_of_the_GNU_Free_Documentation_License  

-- Load required modules  
local yesno = require('Module:Yesno')  

-- Set constants  
local DATA_MODULE = 'Module:Template test case/data'  

---
-- Shared methods  
---

local function message(self, key, ...)  
-- This method is added to classes that need to deal with messages from the  
-- config module.  
local msg = self.cfg.msg[key]  
if select(1, ...) then  
return mw.message.newRawMessage(msg, ...):plain  
else  
return msg  
end  
end  

---
-- Template class  
---

local Template = {}  

Template.memoizedMethods = {  
-- Names of methods to be memoized in each object. This table should only  
-- hold methods with no parameters.  
getFullPage = true,  
getName = true,  
makeHeader = true,  
getOutput = true  
}  

function Template.new(invocationObj, options)  
local obj = {}  

-- Set input  
for k, v in pairs(options or {}) do  
if not Template[k] then  
obj[k] = v  
end  
end  
obj._invocation = invocationObj  

-- Validate input  
if not obj.template and not obj.title then  
error('no template or title specified', 2)  
end  

-- Memoize expensive method calls  
local memoFuncs = {}  
return setmetatable(obj, {  
__index = function (t, key)  
if Template.memoizedMethods[key] then  
local func = memoFuncs[key]  
if not func then  
local val = Template[key](t)  
func = function return val end  
memoFuncs[key] = func  
end  
return func  
else  
return Template[key]  
end  
end  
})  
end  

function Template:getFullPage  
if not self.template then  
return self.title.prefixedText  
elseif self.template:sub(1, 7) == '#invoke' then  
return 'Module' .. self.template:sub(8):gsub('|.*', '')  
else  
local strippedTemplate, hasColon = self.template:gsub('^:', '', 1)  
hasColon = hasColon > 0  
local ns = strippedTemplate:match('^(.-):')  
ns = ns and mw.site.namespaces[ns]  
if ns then  
return strippedTemplate  
elseif hasColon then  
return strippedTemplate -- Main namespace  
else  
return mw.site.namespaces[10].name .. ':' .. strippedTemplate  
end  
end  
end  

function Template:getName  
if self.template then  
return self.template  
else  
return require('Module:Template invocation').name(self.title)  
end  
end  

function Template:makeLink(display)  
if display then  
return string.format('%s', self:getFullPage, display)  
else  
return string.format(':%s', self:getFullPage)  
end  
end  

function Template:makeBraceLink(display)  
display = display or self:getName  
local link = self:makeLink(display)  
return mw.text.nowiki('')  
end  

function Template:makeHeader  
return self.heading or self:makeBraceLink  
end  

function Template:getInvocation(format)  
local invocation = self._invocation:getInvocation{  
template = self:getName,  
requireMagicWord = self.requireMagicWord,  
}  
if format == 'code' then  
invocation = '<code>' .. mw.text.nowiki(invocation) .. '</code>'  
elseif format == 'kbd' then  
invocation = '<kbd>' .. mw.text.nowiki(invocation) .. '</kbd>'  
elseif format == 'plain' then  
invocation = mw.text.nowiki(invocation)  
else  
-- Default is pre tags  
invocation = mw.text.encode(invocation, '&')  
invocation = '<pre>' .. invocation .. '</pre>'  
invocation = mw.getCurrentFrame:preprocess(invocation)  
end  
return invocation  
end  

function Template:getOutput  
local protect = require('Module:Protect')  
-- calling self._invocation:getOutput{...}  
return protect(self._invocation.getOutput)(self._invocation, {  
template = self:getName,  
requireMagicWord = self.requireMagicWord,  
})  
end  

---
-- TestCase class  
---

local TestCase = {}  
TestCase.__index = TestCase  
TestCase.message = message -- add the message method  

TestCase.renderMethods = {  
-- Keys in this table are values of the "format" option, values are the  
-- method for rendering that format.  
columns = 'renderColumns',  
rows = 'renderRows',  
tablerows = 'renderRows',  
inline = 'renderInline',  
cells = 'renderCells',  
default = 'renderDefault'  
}  

function TestCase.new(invocationObj, options, cfg)  
local obj = setmetatable({}, TestCase)  
obj.cfg = cfg  

-- Separate general options from template options. Template options are  
-- numbered, whereas general options are not.  
local generalOptions, templateOptions = {}, {}  
for k, v in pairs(options) do  
local prefix, num  
if type(k) == 'string' then  
prefix, num = k:match('^(.-)([1-9][0-9]*)$')  
end  
if prefix then  
num = tonumber(num)  
templateOptions[num] = templateOptions[num] or {}  
templateOptions[num][prefix] = v  
else  
generalOptions[k] = v  
end  
end  

-- Set general options  
generalOptions.showcode = yesno(generalOptions.showcode)  
generalOptions.showheader = yesno(generalOptions.showheader) ~= false  
generalOptions.showcaption = yesno(generalOptions.showcaption) ~= false  
generalOptions.collapsible = yesno(generalOptions.collapsible)  
generalOptions.notcollapsed = yesno(generalOptions.notcollapsed)  
generalOptions.wantdiff = yesno(generalOptions.wantdiff)  
obj.options = generalOptions  

-- Preprocess template args  
for num, t in pairs(templateOptions) do  
if t.showtemplate ~= nil then  
t.showtemplate = yesno(t.showtemplate)  
end  
end  

-- Set up first two template options tables, so that if only the  
-- "template3" is specified it isn't made the first template when the  
-- the table options array is compressed.  
templateOptions[1] = templateOptions[1] or {}  
templateOptions[2] = templateOptions[2] or {}  

-- Allow the "template" option to override the "template1" option for  
-- backwards compatibility with Module:Testcase table.  
if generalOptions.template then  
templateOptions[1].template = generalOptions.template  
end  

-- Add default template options  
if templateOptions[1].template and not templateOptions[2].template then  
templateOptions[2].template = templateOptions[1].template ..  
'/' .. obj.cfg.sandboxSubpage  
end  
if not templateOptions[1].template then  
templateOptions[1].title = mw.title.getCurrentTitle.basePageTitle  
end  
if not templateOptions[2].template then  
templateOptions[2].title = templateOptions[1].title:subPageTitle(  
obj.cfg.sandboxSubpage  
end  

-- Remove template options for any templates where the showtemplate  
-- argument is false. This prevents any output for that template.  
for num, t in pairs(templateOptions) do  
if t.showtemplate == false then  
templateOptions[num] = nil  
end  
end  

-- Check for missing template names.  
for num, t in pairs(templateOptions) do  
if not t.template and not t.title then  
error(obj:message(  
'missing-template-option-error',  
num, num  
), 2)  
end  
end  

-- Compress templateOptions table so we can iterate over it with ipairs.  
templateOptions = (function (t)  
local nums = {}  
for num in pairs(t) do  
nums[#nums + 1] = num  
end  
table.sort(nums)  
local ret = {}  
for i, num in ipairs(nums) do  
ret[i] = t[num]  
end  
return ret  
end)(templateOptions)  

-- Don't require the __TEMPLATENAME__ magic word for nowiki invocations if  
-- there is only one template being output.  
if #templateOptions <= 1 then  
templateOptions[1].requireMagicWord = false  
end  

mw.logObject(templateOptions)  

-- Make the template objects  
obj.templates = {}  
for i, options in ipairs(templateOptions) do  
table.insert(obj.templates, Template.new(invocationObj, options))  
end  

-- Add tracking categories. At the moment we are only tracking templates  
-- that use any "heading" parameters or an "output" parameter.  
obj.categories = {}  
for k, v in pairs(options) do  
if type(k) == 'string' and k:find('heading') then  
obj.categories['Test cases using heading parameters'] = true  
elseif k == 'output' then  
obj.categories['Test cases using output parameter'] = true  
end  
end  

return obj  
end  

function TestCase:getTemplateOutput(templateObj)  
local output = templateObj:getOutput  
if self.options.resetRefs then  
mw.getCurrentFrame:extensionTag('references')  
end  
return output  
end  

function TestCase:templateOutputIsEqual  
-- Returns a boolean showing whether all of the template outputs are equal.  
-- The random parts of strip markers (see ) are  
-- removed before comparison. This means a strip marker can contain anything  
-- and still be treated as equal, but it solves the problem of otherwise  
-- identical wikitext not returning as exactly equal.  
local function normaliseOutput(obj)  
local out = obj:getOutput  
-- Remove the random parts from strip markers.  
out = out:gsub('(\127[^\127]*UNIQ%-%-%l+%-)%x+(%-%-?QINU[^\127]*\127)', '%1%2')  
return out  
end  
local firstOutput = normaliseOutput(self.templates[1])  
for i = 2, #self.templates do  
local output = normaliseOutput(self.templates[i])  
if output ~= firstOutput then  
return false  
end  
end  
return true  
end  

function TestCase:makeCollapsible(s)  
local title = self.options.title or self.templates[1]:makeHeader  
if self.options.titlecode then  
title = self.templates[1]:getInvocation('kbd')  
end  
local isEqual = self:templateOutputIsEqual  
local root = mw.html.create('div')  
root  
:addClass('mw-collapsible')  
:css('width', '100%')  
:css('', 'solid silver ')  
:css('padding', '0.2em')  
:addClass(self.options.notcollapsed == false and 'mw-collapsed' or nil)  
if self.options.wantdiff then  
root  
:tag('div')  
:css('background-color', isEqual and 'yellow' or '#90a8ee')  
:css('font-weight', 'bold')  
:css('padding', '0.2em')  
:wikitext(title)  
:done  
else  
if self.options.notcollapsed ~= true or false then  
root  
:addClass(isEqual and 'mw-collapsed' or nil)  
end  
root  
:tag('div')  
:css('background-color', isEqual and 'lightgreen' or 'yellow')  
:css('font-weight', 'bold')  
:css('padding', '0.2em')  
:wikitext(title)  
:done  
end  
root  
:tag('div')  
:addClass('mw-collapsible-content')  
:newline  
:wikitext(s)  
:newline  
return tostring(root)  
end  

function TestCase:renderColumns  
local root = mw.html.create  
if self.options.showcode then  
root  
:wikitext(self.templates[1]:getInvocation)  
:newline  
end  

local tableroot = root:tag('table')  

if self.options.showheader then  
-- Caption  
if self.options.showcaption then  
tableroot  
:addClass(self.options.class)  
:cssText(self.options.style)  
:tag('caption')  
:wikitext(self.options.caption or self:message('columns-header'))  
end  

-- Headers  
local headerRow = tableroot:tag('tr')  
if self.options.rowheader then  
-- rowheader is correct here. We need to add another th cell if  
-- rowheader is set further down, even if heading0 is missing.  
headerRow:tag('th'):wikitext(self.options.heading0)  
end  
local width  
if #self.templates > 0 then  
width = tostring(math.floor(100 / #self.templates)) .. '%'  
else  
end  
for i, obj in ipairs(self.templates) do  
headerRow  
:tag('th')  
:css('width', width)  
:wikitext(obj:makeHeader)  
end  
end  

-- Row header  
local dataRow = tableroot:tag('tr'):css('vertical-align', 'top')  
if self.options.rowheader then  
dataRow:tag('th')  
:attr('scope', 'row')  
:wikitext(self.options.rowheader)  
end  

-- Template output  
for i, obj in ipairs(self.templates) do  
if self.options.output == 'nowiki+' then  
dataRow:tag('td')  
:newline  
:wikitext(self.options.before)  
:wikitext(self:getTemplateOutput(obj))  
:wikitext(self.options.after)  
:wikitext('<pre>')  
:wikitext(mw.text.nowiki(self.options.before or ""))  
:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))  
:wikitext(mw.text.nowiki(self.options.after or ""))  
:wikitext('</pre>')  
elseif self.options.output == 'nowiki' then  
dataRow:tag('td')  
:newline  
:wikitext(mw.text.nowiki(self.options.before or ""))  
:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))  
:wikitext(mw.text.nowiki(self.options.after or ""))  
else  
dataRow:tag('td')  
:newline  
:wikitext(self.options.before)  
:wikitext(self:getTemplateOutput(obj))  
:wikitext(self.options.after)  
end  
end  

return tostring(root)  
end  

function TestCase:renderRows  
local root = mw.html.create  
if self.options.showcode then  
root  
:wikitext(self.templates[1]:getInvocation)  
:newline  
end  

local tableroot = root:tag('table')  
tableroot  
:addClass(self.options.class)  
:cssText(self.options.style)  

if self.options.caption then  
tableroot  
:tag('caption')  
:wikitext(self.options.caption)  
end  

for _, obj in ipairs(self.templates) do  
local dataRow = tableroot:tag('tr')  

-- Header  
if self.options.showheader then  
if self.options.format == 'tablerows' then  
dataRow:tag('th')  
:attr('scope', 'row')  
:css('vertical-align', 'top')  
:css('text-align', '')  
:wikitext(obj:makeHeader)  
dataRow:tag('td')  
:css('vertical-align', 'top')  
:css('padding', '0 1em')  
:wikitext('→')  
else  
dataRow:tag('td')  
:css('text-align', '')  
:css('font-weight', 'bold')  
:wikitext(obj:makeHeader)  
dataRow = tableroot:tag('tr')  
end  
end  

-- Template output  
if self.options.output == 'nowiki+' then  
dataRow:tag('td')  
:newline  
:wikitext(self:getTemplateOutput(obj))  
:wikitext('<pre>')  
:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))  
:wikitext('</pre>')  
elseif self.options.output == 'nowiki' then  
dataRow:tag('td')  
:newline  
:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))  
else  
dataRow:tag('td')  
:newline  
:wikitext(self:getTemplateOutput(obj))  
end  
end  

return tostring(root)  
end  

function TestCase:renderInline  
local arrow = mw.language.getContentLanguage:getArrow('forwards')  
local ret = {}  
for i, obj in ipairs(self.templates) do  
local line = {}  
line[#line + 1] = self.options.prefix or '* '  
if self.options.showcode then  
line[#line + 1] = obj:getInvocation('code')  
line[#line + 1] = ' '  
line[#line + 1] = arrow  
line[#line + 1] = ' '  
end  
if self.options.output == 'nowiki+' then  
line[#line + 1] = self:getTemplateOutput(obj)  
line[#line + 1] = '<pre>'  
line[#line + 1] = mw.text.nowiki(self:getTemplateOutput(obj))  
line[#line + 1] = '</pre>'  
elseif self.options.output == 'nowiki' then  
line[#line + 1] = mw.text.nowiki(self:getTemplateOutput(obj))  
else  
line[#line + 1] = self:getTemplateOutput(obj)  
end  
ret[#ret + 1] = table.concat(line)  
end  
if self.options.addline then  
local line = {}  
line[#line + 1] = self.options.prefix or '* '  
line[#line + 1] = self.options.addline  
ret[#ret + 1] = table.concat(line)  
end  
return table.concat(ret, '\n')  
end  

function TestCase:renderCells  
local root = mw.html.create  
local dataRow = root:tag('tr')  
dataRow  
:css('vertical-align', 'top')  
:addClass(self.options.class)  
:cssText(self.options.style)  

-- Row header  
if self.options.rowheader then  
dataRow:tag('th')  
:attr('scope', 'row')  
:newline  
:wikitext(self.options.rowheader or self:message('row-header'))  
end  
-- Caption  
if self.options.showcaption then  
dataRow:tag('th')  
:attr('scope', 'row')  
:newline  
:wikitext(self.options.caption or self:message('columns-header'))  
end  

-- Show code  
if self.options.showcode then  
dataRow:tag('td')  
:newline  
:wikitext(self:getInvocation('code'))  
end  

-- Template output  
for i, obj in ipairs(self.templates) do  
if self.options.output == 'nowiki+' then  
dataRow:tag('td')  
:newline  
:wikitext(self.options.before)  
:wikitext(self:getTemplateOutput(obj))  
:wikitext(self.options.after)  
:wikitext('<pre>')  
:wikitext(mw.text.nowiki(self.options.before or ""))  
:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))  
:wikitext(mw.text.nowiki(self.options.after or ""))  
:wikitext('</pre>')  
elseif self.options.output == 'nowiki' then  
dataRow:tag('td')  
:newline  
:wikitext(mw.text.nowiki(self.options.before or ""))  
:wikitext(mw.text.nowiki(self:getTemplateOutput(obj)))  
:wikitext(mw.text.nowiki(self.options.after or ""))  
else  
dataRow:tag('td')  
:newline  
:wikitext(self.options.before)  
:wikitext(self:getTemplateOutput(obj))  
:wikitext(self.options.after)  
end  
end  

return tostring(root)  
end  

function TestCase:renderDefault  
local ret = {}  
if self.options.showcode then  
ret[#ret + 1] = self.templates[1]:getInvocation  
end  
for i, obj in ipairs(self.templates) do  
ret[#ret + 1] = '<div></div>'  
if self.options.showheader then  
ret[#ret + 1] = obj:makeHeader  
end  
if self.options.output == 'nowiki+' then  
ret[#ret + 1] = self:getTemplateOutput(obj) ..  
'<pre>' ..  
mw.text.nowiki(self:getTemplateOutput(obj)) .. '</pre>'  
elseif self.options.output == 'nowiki' then  
ret[#ret + 1] = mw.text.nowiki(self:getTemplateOutput(obj))  
else  
ret[#ret + 1] = self:getTemplateOutput(obj)  
end  
end  
return table.concat(ret, '\n\n')  
end  

function TestCase:__tostring  
local format = self.options.format  
local method = format and TestCase.renderMethods[format] or 'renderDefault'  
local ret = self[method](self)  
if self.options.collapsible then  
ret = self:makeCollapsible(ret)  
end  
for cat in pairs(self.categories) do  
ret = ret .. string.format('', cat)  
end  
return ret  
end  

---
-- Nowiki invocation class  
---

local NowikiInvocation = {}  
NowikiInvocation.__index = NowikiInvocation  
NowikiInvocation.message = message -- Add the message method  

function NowikiInvocation.new(invocation, cfg)  
local obj = setmetatable({}, NowikiInvocation)  
obj.cfg = cfg  
invocation = mw.text.unstrip(invocation)  
-- Decode HTML entities for <, >, and ". This means that HTML entities in  
-- the original code must be escaped as e.g. &amp;lt;, which is unfortunate,  
-- but it is the best we can do as the distinction between <, >, " and &lt;,  
-- &gt;, &quot; is lost during the original nowiki operation.  
invocation = invocation:gsub('&lt;', '<')  
invocation = invocation:gsub('&gt;', '>')  
invocation = invocation:gsub('&quot;', '"')  
obj.invocation = invocation  
return obj  
end  

function NowikiInvocation:getInvocation(options)  
local template = options.template:gsub('%%', '%%%%') -- Escape "%" with "%%"  
local invocation, count = self.invocation:gsub(  
self.cfg.templateNameMagicWordPattern,  
template  
if options.requireMagicWord ~= false and count < 1 then  
error(self:message(  
'nowiki-magic-word-error',  
self.cfg.templateNameMagicWord  
))  
end  
return invocation  
end  

function NowikiInvocation:getOutput(options)  
local invocation = self:getInvocation(options)  
return mw.getCurrentFrame:preprocess(invocation)  
end  

---
-- Table invocation class  
---

local TableInvocation = {}  
TableInvocation.__index = TableInvocation  
TableInvocation.message = message -- Add the message method  

function TableInvocation.new(invokeArgs, nowikiCode, cfg)  
local obj = setmetatable({}, TableInvocation)  
obj.cfg = cfg  
obj.invokeArgs = invokeArgs  
obj.code = nowikiCode  
return obj  
end  

function TableInvocation:getInvocation(options)  
if self.code then  
local nowikiObj = NowikiInvocation.new(self.code, self.cfg)  
return nowikiObj:getInvocation(options)  
else  
return require('Module:Template invocation').invocation(  
options.template,  
self.invokeArgs  
end  
end  

function TableInvocation:getOutput(options)  
if (options.template:sub(1, 7) == '#invoke') then  
local moduleCall = mw.text.split(options.template, '|', true)  
local args = mw.clone(self.invokeArgs)  
table.insert(args, 1, moduleCall[2])  
return mw.getCurrentFrame:callParserFunction(moduleCall[1], args)  
end  
return mw.getCurrentFrame:expandTemplate{  
title = options.template,  
args = self.invokeArgs  
}  
end  

---
-- Bridge functions  
---
-- These functions translate template arguments into forms that can be accepted  
-- by the different classes, and return the results.  
---

local bridge = {}  

function bridge.table(args, cfg)  
cfg = cfg or mw.loadData(DATA_MODULE)  

local options, invokeArgs = {}, {}  
for k, v in pairs(args) do  
local optionKey = type(k) == 'string' and k:match('^_(.*)$')  
if optionKey then  
if type(v) == 'string' then  
v = v:match('^%s*(.-)%s*$') -- trim whitespace  
end  
if v ~= '' then  
options[optionKey] = v  
end  
else  
invokeArgs[k] = v  
end  
end  

-- Allow passing a nowiki invocation as an option. While this means users  
-- have to pass in the code twice, whitespace is preserved and &lt; etc.  
-- will work as intended.  
local nowikiCode = options.code  
options.code = nil  

local invocationObj = TableInvocation.new(invokeArgs, nowikiCode, cfg)  
local testCaseObj = TestCase.new(invocationObj, options, cfg)  
return tostring(testCaseObj)  
end  

function bridge.nowiki(args, cfg)  
cfg = cfg or mw.loadData(DATA_MODULE)  

local code = args.code or args[1]  
local invocationObj = NowikiInvocation.new(code, cfg)  
args.code = nil  
args[1] = nil  
-- Assume we want to see the code as we already passed it in.  
args.showcode = args.showcode or true  
local testCaseObj = TestCase.new(invocationObj, args, cfg)  
return tostring(testCaseObj)  
end  

---
-- Exports  
---

local p = {}  

function p.main(, cfg)  
cfg = cfg or mw.loadData(DATA_MODULE)  

-- Load the wrapper config, if any.  
local wrapperConfig  
if .getParent then  
local title = :getParent:getTitle  
local template = title:gsub(cfg.sandboxSubpagePattern, '')  
wrapperConfig = cfg.wrappers[template]  
end  

-- Work out the function we will call, use it to generate the config for  
-- Module:Arguments, and use Module:Arguments to find the arguments passed  
-- by the user.  
local func = wrapperConfig and wrapperConfig.func or 'table'  
local userArgs = require('Module:Arguments').getArgs(, {  
parentOnly = wrapperConfig,  
frameOnly = not wrapperConfig,  
trim = func ~= 'table',  
removeBlanks = func ~= 'table'  
})  

-- Get default args and build the args table. User-specified args overwrite  
-- default args.  
local defaultArgs = wrapperConfig and wrapperConfig.args or {}  
local args = {}  
for k, v in pairs(defaultArgs) do  
args[k] = v  
end  
for k, v in pairs(userArgs) do  
args[k] = v  
end  

return bridge[func](args, cfg)  
end  

function p._exportClasses -- For testing  
return {  
Template = Template,  
TestCase = TestCase,  
NowikiInvocation = NowikiInvocation,  
TableInvocation = TableInvocation  
}  
end  

return p