Revision as of 06:07, 3 December 2014 view sourceMr. Stradivarius (talk | contribs)Edit filter managers, Administrators59,191 edits allow args as an alias for args.code for nowiki invocations← Previous edit | Revision as of 00:45, 8 December 2014 view source Mr. Stradivarius (talk | contribs)Edit filter managers, Administrators59,191 editsm copy edit the noticeNext edit → | ||
(One intermediate revision by the same user not shown) | |||
Line 1: | Line 1: | ||
--[[ | |||
-- This module provides several methods to generate test cases. | |||
A module for generating test case templates. | |||
This module incorporates code from the English Misplaced Pages's "Testcase table" | |||
module, written by Frietjes with contributions by Mr. Stradivarius | |||
and Jackmcbarn, and the English Misplaced Pages's "Testcase rows" module, | |||
written by Mr. Stradivarius. | |||
The "Testcase table" and "Testcase rows" modules are released under the | |||
CC BY-SA 3.0 License and the GFDL. | |||
License: CC BY-SA 3.0 and the GFDL | |||
Author: Mr. Stradivarius | |||
https://en.wikipedia.org/Module:Testcase_table | |||
https://en.wikipedia.org/User:Frietjes | |||
https://en.wikipedia.org/User:Mr._Stradivarius | |||
https://en.wikipedia.org/User:Jackmcbarn | |||
https://en.wikipedia.org/Module:Testcase_rows | |||
https://en.wikipedia.org/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License | |||
https://en.wikipedia.org/Wikipedia:Text_of_the_GNU_Free_Documentation_License | |||
]] | |||
-- Load required modules | -- Load required modules |
Revision as of 00:45, 8 December 2014
Module documentation[view] [edit] [history] [purge]This module depends on the following other modules: |
This module provides a framework for making templates which produce a template test case. While test cases can be made manually, using Lua-based templates such as the ones provided by this module has the advantage that the template arguments only need to be input once, thus reducing the effort involved in making test cases and reducing the possibility of errors in the input.
Usage
This module should not usually be called directly. Instead, you should use one of the following templates:
Parameter-based templates:
- Template:Test case – for standard test cases
- Template:Testcase table – for test cases arranged side by side in columns
- Template:Testcase rows – for test cases arranged as rows in a table
- Template:Collapsible test case – for test cases that are collapsed by default if the results are the same
- Template:Inline test case – for test cases with small invocations and small output, that do not contain any line breaks
The only difference between these templates is their default arguments. For example, it is possible to display test cases side by side in Template:Testcase rows by specifying |_format=columns
Nowiki-based templates:
- Template:Test case nowiki – for test cases created from template code wrapped in nowiki tags (useful for displaying complex template invocations)
- Template:Nowiki template demo – for use in template documentation
It is also possible to use a format of {{#invoke:template test case|main|parameters}}
. This uses the same defaults as Template:Test case; please see that page for documentation of the parameters.
There is no direct interface to this module for other Lua modules. Lua modules should generally use Lua-based test case modules such as Module:UnitTests or Module:ScribuntoUnit. If it is really necessary to use this module, you can use frame:expandTemplate with one of the templates listed above.
Configuration
This module has a configuration module at Module:Template test case/config. You can edit it to add new wrapper templates, or to change the messages that the module outputs.
Tracking categories
The above documentation is transcluded from Module:Template test case/doc. (edit | history)Editors can experiment in this module's sandbox (edit | diff) and testcases (edit | run) pages.
Subpages of this module.
--[[ A module for generating test case templates. This module incorporates code from the English Misplaced Pages's "Testcase table" module, written by Frietjes with contributions by Mr. Stradivarius and Jackmcbarn, and the English Misplaced Pages's "Testcase rows" module, written by Mr. Stradivarius. The "Testcase table" and "Testcase rows" modules are released under the CC BY-SA 3.0 License and the GFDL. License: CC BY-SA 3.0 and the GFDL Author: Mr. Stradivarius https://en.wikipedia.org/Module:Testcase_table https://en.wikipedia.org/User:Frietjes https://en.wikipedia.org/User:Mr._Stradivarius https://en.wikipedia.org/User:Jackmcbarn https://en.wikipedia.org/Module:Testcase_rows https://en.wikipedia.org/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License https://en.wikipedia.org/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 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, makeHeading = true, getOutput = true } function Template.new(invocationObj, options) local obj = {} -- Set input for k, v in pairs(options or {}) do if not Template then obj = 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 then local func = memoFuncs if not func then local val = Template(t) func = function () return val end memoFuncs = func end return func else return Template end end }) end function Template:getFullPage() if self.template then local strippedTemplate, hasColon = self.template:gsub('^:', '', 1) hasColon = hasColon > 0 local ns = strippedTemplate:match('^(.-):') ns = ns and mw.site.namespaces if ns then return strippedTemplate elseif hasColon then return strippedTemplate -- Main namespace else return mw.site.namespaces.name .. ':' .. strippedTemplate end else return self.title.prefixedText 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(']', self:getFullPage(), display) else return string.format(']', 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:makeHeading() return self.heading or self:makeBraceLink() end function Template:getInvocation(format) local invocation = self._invocation:getInvocation(self:getName()) if format == 'code' then invocation = '<code>' .. mw.text.nowiki(invocation) .. '</code>' 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() return self._invocation:getOutput(self:getName()) 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', inline = 'renderInline', 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 = {}, {} do local optionNum = {} -- a unique key for option numbers inside templateOptions local rawTemplateOptions = {} for k, v in pairs(options) do local prefix, num if type(k) == 'string' then prefix, num = k:match('^(.-)(*)$') end if prefix then num = tonumber(num) rawTemplateOptions = rawTemplateOptions or {} rawTemplateOptions = v rawTemplateOptions = num -- record for use in error messages else generalOptions = v 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. rawTemplateOptions = rawTemplateOptions or {} rawTemplateOptions = rawTemplateOptions or {} -- Allow the "template" option to override the "template1" option for -- backwards compatibility with ]. rawTemplateOptions.template = generalOptions.template or rawTemplateOptions.template -- Add default template options if rawTemplateOptions.template and not rawTemplateOptions.template then rawTemplateOptions.template = rawTemplateOptions.template .. '/' .. obj.cfg.sandboxSubpage end if not rawTemplateOptions.template then rawTemplateOptions.title = mw.title.getCurrentTitle().basePageTitle end if not rawTemplateOptions.template then rawTemplateOptions.title = rawTemplateOptions.title:subPageTitle( obj.cfg.sandboxSubpage ) end -- Remove gaps in the numbered options local nums = {} for num in pairs(rawTemplateOptions) do nums = num end table.sort(nums) for i, num in ipairs(nums) do templateOptions = rawTemplateOptions end -- Check that there are no missing template options. for i = 3, #templateOptions do -- Defaults have already been added for 1 and 2. local t = templateOptions if not t.template then local num = t error(obj:message( 'missing-template-option-error', num, num ), 2) end end end -- Set general options generalOptions.showcode = yesno(generalOptions.showcode) generalOptions.collapsible = yesno(generalOptions.collapsible) obj.options = generalOptions -- Make the template objects obj.templates = {} for i, t in ipairs(templateOptions) do table.insert(obj.templates, Template.new(invocationObj, t)) 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('(%cUNIQ).-(QINU%c)', '%1%2') return out end local firstOutput = normaliseOutput(self.templates) for i = 2, #self.templates do local output = normaliseOutput(self.templates) if output ~= firstOutput then return false end end return true end function TestCase:makeCollapsible(s) local isEqual = self:templateOutputIsEqual() local root = mw.html.create('table') root :addClass('collapsible') :addClass(isEqual and 'collapsed' or nil) :css('background-color', 'transparent') :css('width', '100%') :css('border', 'solid silver 1px') :tag('tr') :tag('th') :css('background-color', isEqual and 'lightgreen' or 'yellow') :wikitext(self.options.title or self.templates:makeHeading()) :done() :done() :tag('tr') :tag('td') :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:getInvocation()) :newline() end local tableroot = root:tag('table') tableroot :addClass(self.options.class) :cssText(self.options.style) :tag('caption') :wikitext(self.options.caption or self:message('columns-header')) -- Headings local headingRow = 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. headingRow: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 headingRow :tag('th') :css('width', width) :wikitext(obj:makeHeading()) 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 dataRow:tag('td') :newline() :wikitext(self:getTemplateOutput(obj)) :wikitext(self.options.after) end return tostring(root) end function TestCase:renderRows() local root = mw.html.create() if self.options.showcode then root :wikitext(self.templates: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 -- Build the row HTML tableroot :tag('tr') :tag('td') :css('text-align', 'center') :css('font-weight', 'bold') :wikitext(obj:makeHeading()) :done() :done() :tag('tr') :tag('td') :newline() :wikitext(self:getTemplateOutput(obj)) 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 = '* ' if self.options.showcode then line = obj:getInvocation('code') line = ' ' line = arrow line = ' ' end line = self:getTemplateOutput(obj) ret = table.concat(line) end return table.concat(ret, '\n') end function TestCase:renderDefault() local ret = {} if self.options.showcode then ret = self.templates:getInvocation() end for i, obj in ipairs(self.templates) do ret = '<div style="clear: both;"></div>' ret = obj:makeBraceLink() ret = self:getTemplateOutput(obj) end return table.concat(ret, '\n\n') end function TestCase:__tostring() local format = self.options.format local method = format and TestCase.renderMethods or 'renderDefault' local ret = self(self) if self.options.collapsible then ret = self:makeCollapsible(ret) 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(template) template = template:gsub('%%', '%%%%') -- Escape "%" with "%%" local invocation, count = self.invocation:gsub( self.cfg.templateNameMagicWordPattern, template ) if count < 1 then error(self:message( 'nowiki-magic-word-error', self.cfg.templateNameMagicWord )) end return invocation end function NowikiInvocation:getOutput(template) local invocation = self:getInvocation(template) 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(template) if self.code then local nowikiObj = NowikiInvocation.new(self.code, self.cfg) return nowikiObj:getInvocation(template) else return require('Module:Template invocation').invocation( template, self.invokeArgs ) end end function TableInvocation:getOutput(template) return mw.getCurrentFrame():expandTemplate{ title = 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 = v end else invokeArgs = 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) local code = args.code or args local invocationObj = NowikiInvocation.new(code, cfg) args.code = nil args = 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(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 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 = v end for k, v in pairs(userArgs) do args = v end return bridge(args, cfg) end function p._exportClasses() -- For testing return { Template = Template, TestCase = TestCase, NowikiInvocation = NowikiInvocation, TableInvocation = TableInvocation } end return pCategories: