Misplaced Pages

Module:Template test case: Difference between revisions

Article snapshot taken from Wikipedia with creative commons attribution-sharealike license. Give it a read and then ask your questions in the chat. We can research this topic together.
Browse history interactively← Previous editNext edit →Content deleted Content added
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:

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:

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. &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(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 &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
	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 p
Categories: