Jump to content
Main menu
Main menu
move to sidebar
hide
Navigation
Main page
Recent changes
Random page
Help about MediaWiki
Search
Search
Create account
Log in
Personal tools
Create account
Log in
Pages for logged out editors
learn more
Contributions
Talk
Editing
Module:Template test case
Module
Discussion
English
Read
Edit source
View history
Tools
Tools
move to sidebar
hide
Actions
Read
Edit source
View history
Move
General
What links here
Related changes
Special pages
Page information
Warning:
You are not logged in. Your IP address will be publicly visible if you make any edits. If you
log in
or
create an account
, your edits will be attributed to your username, along with other benefits.
Anti-spam check. Do
not
fill this in!
--[[ 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|%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('{{') .. link .. 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 style="white-space: pre-wrap;">' .. 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 [[Help:Strip markers]]) 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('border', 'solid silver 1px') :css('padding', '0.2em') :css('clear', 'both') :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('color', 'black') :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('color', 'black') :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 width = '100%' 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 style="white-space: pre-wrap;">') :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', 'left') :wikitext(obj:makeHeader()) dataRow:tag('td') :css('vertical-align', 'top') :css('padding', '0 1em') :wikitext('β') else dataRow:tag('td') :css('text-align', 'center') :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.options.before) :wikitext(self:getTemplateOutput(obj)) :wikitext(self.options.after) :wikitext('<pre style="white-space: pre-wrap;">') :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: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.options.before or "" line[#line + 1] = self:getTemplateOutput(obj) line[#line + 1] = self.options.after or "" line[#line + 1] = '<pre style="white-space: pre-wrap;">' line[#line + 1] = mw.text.nowiki(self.options.before or "") line[#line + 1] = mw.text.nowiki(self:getTemplateOutput(obj)) line[#line + 1] = mw.text.nowiki(self.options.after or "") line[#line + 1] = '</pre>' elseif self.options.output == 'nowiki' then line[#line + 1] = mw.text.nowiki(self.options.before or "") line[#line + 1] = mw.text.nowiki(self:getTemplateOutput(obj)) line[#line + 1] = mw.text.nowiki(self.options.after or "") else line[#line + 1] = self.options.before or "" line[#line + 1] = self:getTemplateOutput(obj) line[#line + 1] = self.options.after or "" 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 style="white-space: pre-wrap;">') :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 style="clear: both;"></div>' if self.options.showheader then ret[#ret + 1] = obj:makeHeader() end if self.options.output == 'nowiki+' then ret[#ret + 1] = (self.options.before or "") .. self:getTemplateOutput(obj) .. (self.options.after or "") .. '<pre style="white-space: pre-wrap;">' .. mw.text.nowiki(self.options.before or "") .. mw.text.nowiki(self:getTemplateOutput(obj)) .. mw.text.nowiki(self.options.after or "") .. '</pre>' elseif self.options.output == 'nowiki' then ret[#ret + 1] = mw.text.nowiki(self.options.before or "") .. mw.text.nowiki(self:getTemplateOutput(obj)) .. mw.text.nowiki(self.options.after or "") else ret[#ret + 1] = (self.options.before or "") .. self:getTemplateOutput(obj) .. (self.options.after or "") 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('[[Category:%s]]', 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. &lt;, which is unfortunate, -- but it is the best we can do as the distinction between <, >, " and <, -- >, " is lost during the original nowiki operation. invocation = invocation:gsub('<', '<') invocation = invocation:gsub('>', '>') invocation = invocation:gsub('"', '"') 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 < 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) -- Convert args beginning with _ for consistency with the normal bridge local newArgs = {} for k, v in pairs(args) do local normalName = type(k) == "string" and string.match(k, "^_(.*)$") if normalName then newArgs[normalName] = v else newArgs[k] = v end end local code = newArgs.code or newArgs[1] local invocationObj = NowikiInvocation.new(code, cfg) newArgs.code = nil newArgs[1] = nil -- Assume we want to see the code as we already passed it in. newArgs.showcode = newArgs.showcode or true local testCaseObj = TestCase.new(invocationObj, newArgs, cfg) return tostring(testCaseObj) end ------------------------------------------------------------------------------- -- Exports ------------------------------------------------------------------------------- local p = {} function p.main(frame, cfg) cfg = cfg or mw.loadData(DATA_MODULE) -- Load the wrapper config, if any. local wrapperConfig if frame.getParent then local title = frame: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(frame, { 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
Summary:
Please note that all contributions to Ikwipedia are considered to be released under the Creative Commons Attribution-ShareAlike (see
Ikwipedia:Copyrights
for details). If you do not want your writing to be edited mercilessly and redistributed at will, then do not submit it here.
You are also promising us that you wrote this yourself, or copied it from a public domain or similar free resource.
Do not submit copyrighted work without permission!
Cancel
Editing help
(opens in new window)
Template used on this page:
Module:Template test case/doc
(
edit
)
Toggle limited content width