Vorlagenprogrammierung Diskussionen Lua Unterseiten
Modul Deutsch English

Modul: Dokumentation

Diese Seite enthält Code in der Programmiersprache Lua. Einbindungszahl Cirrus


--[=[  Test 2022-12-10
	test a module
	
	Autor: Vollbracht
	
* test.single(frame)
  {{#invoke:Test|single	|module=<module name> |expected=<html or wikitext>
						|func=<function to be tested>
						|<key=value> |<key=value> ... }}
  {{#invoke:Test|single	|module=<module name> |expected=<html or wikitext>
						|func=<function to be tested>
						|unnamed=<value>{!}<value>{!} ... }}
  {{#invoke:Test|delimitLogs}}
  (generates horrizontal bar in log)

]=]

local p = {}

local USAGE =
[[<code>{{#invoke:Test|single|module=<module name> |expected=<html or wikitext>
|func=<function to be tested> |<key=value> |<key=value> ... }}</code>]]

--[[
	Call
	an object representating whatever is necessary to call a function
	constructors:
		new(frame, action, data)		testcase
		suggester(frame, test, result)	an invoke call of a Test function
	fields:
		frame			object	copy of the calling objects frame
		wikitext		string	available for invoke only
		luatext			string
		modulename		string
		moduleprefix	string	'Module' or 'Modul'
		Module			required module by this.moduleprefix:this.modulename
		functionpath	array of strings
		parameters		array
	methods:
		toString()		returns <nowiki>-wraped wikitext or (if inavailable)
						unwraped luatext with its particular parameters
		toCode()		returns <code>-wraped toString()
]]
local Call = {
	properties = {module=1, func=2, expected=4, unnamed=8, export=16},
	toString = function(this) -- ####
		if this.wikitext then
			return this.frame:extensionTag('nowiki', this.wikitext, {})
		end
		if this.wraper then return this.wraper end
		return this.luatext
	end,
	toCode = function(this)
		return this.frame:extensionTag('code', this:toString(), {})
	end,
}

--[[
	new(frame, action, data)
	constructor for a client call, i. e. a test case
	parameters:
		frame	environmental frame of the test providing call
		action	string representing the module and its directly exported
				function, which is to be tested
				possible formats:
					"<module>/<function path>"
					{modulename=<module name>, functionpath=<function path>}
					{	moduleprefix=<module prefix>, modulename=<module name>,
						functionpath=<function path>	}
					Call
		data	all parameters remaining in test case, which are handed over to
				the client module and function that is to be tested
	returns the new call object
]]
function Call:new(frame, action, data)
	if not frame or not frame.extensionTag or not action then return nil end
	local result = {
		frame = frame,
		parameters = data
	}
	local set, success, extendSearch = false
	if type(action) == 'string' then
		result.Module = nil
		result.functionpath = {}
		set = {action}
		if not action:find('^Modul') then extendSearch = true end
		while not result.Module do
			set = {set[1]:match('(.*)%/(%a%w*)$')}
			if not set[1] then return nil end
			result.functionpath = {set[2], unpack(result.functionpath)}
			success, result.Module = pcall(require, set[1])
			if success then
				if extendSearch then
					result.moduleprefix = ''
					result.modulename = set[1]
				else
					result.moduleprefix, result.modulename
						= set[1]:match('(Modul[e]?%:)(.+)')
				end
			elseif extendSearch then
				success, result.Module = pcall(require, 'Modul:' .. set[1])
				if result.Module then
					result.modulename = set[1]
					result.moduleprefix = 'Modul:'
				else
					success, result.Module = pcall(require, 'Module:' .. set[1])
					if result.Module then
						result.modulename = set[1]
						result.moduleprefix = 'Module:'
					end
				end
			else return nil
			end
		end
	elseif type(action) == 'table' then
		if not action.modulename then return nil end
		if not action.functionpath then return nil end
		result.modulename = action.modulename
		result.functionpath = functionpath
		if action.Module then
			result.Module = action.Module
			if action.moduleprefix then
				result.moduleprefix = action.moduleprefix
			end
		else
			if action.moduleprefix then
				result.moduleprefix = action.moduleprefix
				success, result.Module = pcall(require,
									result.moduleprefix .. result.modulename)
				if not success then return nil end
			else
				success, result.Module = pcall(require, result.modulename)
				if not success then
					local mn = 'Modul:' .. result.modulename
					success, result.Module = pcall(require, mn)
					if success then result.moduleprefix = 'Modul:'
					else
						local mn = 'Module:' .. result.modulename
						success, result.Module = pcall(require, mn)
						if success then result.moduleprefix = 'Module:'
						else return nil end
					end
				end
			end
		end
	end
	local luadata = ''
	local wtData = ''
	for i, v in ipairs(data) do
		luadata = luadata .. ', [' .. i .. '] = "' .. v .. '"'
		wtData = wtData .. '|' .. v
	end
	for k, v in pairs(data) do
		luadata = luadata .. ', ' .. k .. ' = "' .. v .. '"'
		wtData = wtData .. '|' .. k .. '=' .. v .. ' '
	end
	if luadata == '' then
		result.luatext =	table.concat(result.functionpath, '/') .. '()'
		if #result.functionpath == 1 then
			result.wikitext =	'{{#invoke:' .. result.modulename .. ' |'
							..	result.functionpath[1] .. '}}'
		end
	else
		result.luatext =	table.concat(result.functionpath, '/') .. '{'
						..	luadata:sub(3) .. '}'
		if #result.functionpath == 1 then
			result.wikitext =	'{{#invoke:' .. result.modulename .. ' |'
							..	result.functionpath[1]
							..	wtData:gsub('%s*$', '') .. '}}'
		end
	end
	setmetatable(result, self)
	self.__index = self
	return result
end


--[[
	suggester(frame, test, suggestion)
	constructor for a test call
	parameters:
		frame	environmental frame of the test providing call
		test	name of the used test function in Test module
		suggestion	suggested result for the tested function
	returns the new call object
]]
function Call:suggester(frame, test, suggestion)
	local result = {
		frame = frame,
		modulename = 'Test',
		functionpath = { test },
		parameters = frame.args
	}
	local luadata = ''
	local wtData = ''
	for i, v in ipairs(frame.args) do
		luadata = luadata .. ', [' .. i .. '] = "' .. v .. '"'
		wtData = wtData .. '|' .. v
	end
	for k, v in pairs(frame.args) do
		if not k:find('^%d+$') then
			luadata = luadata .. ', ' .. k .. ' = "' .. v .. '"'
			wtData = wtData .. '|' .. k .. '=' .. v .. ' '
		end
	end
	luadata = luadata .. ', expected = "' .. suggestion .. '"'
	wtData = wtData .. '|expected=' .. suggestion
	result.luatext = test .. '{' .. luadata:sub(3) .. '}'
	result.wikitext =	'{{#invoke:Test |' .. test .. ' ' .. wtData .. ' }}'
	setmetatable(result, self)
	self.__index = self
	return result
end

--[[
	Counter
	Object: value set of prefix, number and postfix with an optional key name
	
]]
local Counter = {
	use = function(this)
		local result = this.prefix .. tostring(this.value) .. this.postfix
		this.value = this.value + 1
		return result
	end,
	keyValue = function(this, index)
		return index + 1, this.prefix .. index .. this.postfix
	end
}
function Counter:new(source, key)
	if not source then return nil end
	local result = {}
	if key then result.key = key end
	if type(source) == 'string' then
		result.prefix, result.value, result.postfix
			= source:match('(.*)%<counter>(%d*)%<%/counter%>(.*)')
		if result.value then
			if result.value == '' then result.value = 0
			else result.value = tonumber(result.value) end
		else
			result.prefix, result.postfix
				= source:match('(.*)%<counter%s?%/%>(.*)')
			if not result.prefix then return nil end
			result.value = 0
		end
	end
	if type(source) == 'table' then result = source end
	if not result.prefix or not result.value or not result.postfix then
		return nil
	end
	setmetatable(result, self)
	self.__index = self
	return result
end

--[[
	Client
	a test type object
	constructor:
		new(frame)
	fields:
		call		a Call object
		expected	string for comparison
		actual		string or an object with a __tostring method
		(	Following fields are for internal use only, because they aren't
			balanced. Use get<Field>() instead!	)
		accordance1	wikitext - do not get
		accordance2	wikitext - do not get
		missedMark	wikitext - get includes accordances
		variation	wikitext - get includes accordances
	methods:
		getVariation()	colored wikitext output of variation
						including accordances
		getMissedMark()	colored wikitext output of missedMark
						including accordances
]]
local Client = {
	getVariation = function(this)
		if not this.variation then return '' end
		local result = this.accordance1
		if result then
			if result ~= '' then
				result = this.call.frame:extensionTag('span',
						this.call.frame:extensionTag('nowiki', result, {}),
						{ style="background-color:#bfa;" })
			end
		else result = '' end
		result = result
			..	this.call.frame:extensionTag('span',
					this.call.frame:extensionTag('nowiki', this.variation, {}),
				{ style="background-color:#fba;" })
		if not this.accordance2 or this.accordance2 == '' then
			return this.call.frame:extensionTag('code', result, {}) end
		return this.call.frame:extensionTag('code', result
			..	this.call.frame:extensionTag('span',
					this.call.frame:extensionTag(	'nowiki', this.accordance2,
													{}),
				{ style="background-color:#bfa;" }), {})
	end,
	getMissedMark = function(this)
		if not this.missedMark or this.missedMark == '' then
			return this.call.frame:extensionTag('code', 
				this.call.frame:extensionTag('span',
				this.call.frame:extensionTag('nowiki', this.expected, {}),
				{ style="background-color:#bfa;" }), {})
		end
		local result = this.accordance1
		if result then
			if result ~= '' then
				result = this.call.frame:extensionTag('span',
					this.call.frame:extensionTag('nowiki', result, {}),
					{ style="background-color:#bfa;" })
			end
		else result = '' end
		result =	result
			..	this.call.frame:extensionTag('nowiki', this.missedMark, {})
		if not this.accordance2 or this.accordance2 == '' then
			return this.call.frame:extensionTag('code', result, {}) end
		return this.call.frame:extensionTag('code', result
			..	this.call.frame:extensionTag('span',
					this.call.frame:extensionTag(	'nowiki', this.accordance2,
													{}),
				{ style="background-color:#bfa;" }), {})
	end,
	toString = function(this) -- ####
		if this.variation then return this:getVariation() end
		return this.actual
	end,
	reducedRows = function(this, test, processCall)
		local result = '<tr><td>'
		if processCall then 
			result = this.call:toString() .. '</td></tr><tr><td>'
		end
		if this.expected then
			return result .. this:toString() .. '</td></tr><tr><td>'
				.. this:getMissedMark() .. '</td></tr>'
		end
		if test then
			local call = Call:suggester(this.call.frame, test, this.actual)
			return	result
				..	this.call.frame:extensionTag(	'syntaxhighlight',
													call.wikitext, {
														lang="html+handlebars"
													})
				..	'</td></tr>'
		end
		return result
			.. this.call.frame:extensionTag('nowiki', this.actual, {})
			.. '</td></tr>'
	end
}

function Client:new(frame)
	local result = {}
	local data = {}
	if #frame.args > 0 then data = {unpack(frame.args)} end
	local action = nil
	-- 1.: unwrap action and expectation from frame.args
	for k, v in pairs(frame.args) do
		if k == 'action' then
			-- extract 1st action only; actions may be delimited by '//'
			v = v:gsub('^%/*', '')
			local i = v:find('//')
			if i then
				action = v:sub(1, i-1)
				if #v > i+2 then v = v:sub(i+2)
				else v = nil end
			else
				action = v
				v = nil
			end
		elseif k == 'expected' then
			-- extract 1st expectation only;
			-- expectations may be delimited by '<split></split>'
			local i, j = v:find('.%<split%>%<%/split%>.')
			if i then
				result.expected = v:sub(1, i - 1)
				if #v > j+1 then v = v:sub(j + 1)
				else v = nil end
			else
				result.expected = v
				v = nil
			end
		else
			local s, c = v:gsub('%<inTestSeries%s*%>?%<?%/%>?', '')
			if c > 0 then
				s = s:gsub('inTestSeries%>', '')
				if not result.testSeries then result.testSeries = {} end
				table.insert(result.testSeries, k)
				v = s
			elseif v:find('%<counter') then
				if not result.counters then result.counters = {} end
				local c = Counter:new(v, k)
				if c then
					table.insert(result.counters, c)
					v = c:use()
				end
			end
		end
		if v then data[k] = v end
	end
	if not action then
		return {
			Error = 'Missing argument "action=<module name>/<function name>".'
		}
	end
	-- 2.: generate string representation from action
	result.call = Call:new(frame, action, data)
	local e = ''
	if not result.call then
		e =	'Inappropriate argument "action" is "' .. action
		..	'" but should be "<module name>/<function name>" instead.'
		return { Error = e }
	end
	mw.logObject(result, 'result')
	-- 3.: retreive executable function from action
	local Function = result.call.Module
	if not Function then
		e =	'Inappropriate module name "' .. result.call.modulename
		..	'" in test call.'
		if not result.call.moduleprefix then
			if result.call.wikitext then
				e = e .. ' (Even with "Module:" or "Modul:" prefix!)'
			else
				e = e .. ' Try providing a "Module:" or "Modul:" prefix!'
			end
		end
		return { Error = e }
	end
	for _, v in ipairs(result.call.functionpath) do
		Function = Function[v]
		if not Function then
			e =	'Inavailable export "' .. v
			..	'" in ' .. result.call.luatext .. ' call.'
			return { Error = e }
		end
	end
	-- 4.: get actual result of execution of this function
	local handler = function(err)
		e = err
		mw.logObject(e, 'error in execution of ' .. result.call.luatext)
	end
	local success = false
	if result.call.wikitext then
		mw.logObject(result, 'try wikitext')
		local c = result.call
		local f =  frame:newChild({ title = c.modulename, args=c.parameters })
		fkt = function()
			return Function(f)
		end
		success, result.actual = xpcall(fkt, handler, {})
	end
	if not success then
		mw.logObject(result, 'try without wikitext')
		success, result.actual = xpcall(Function, handler, unpack(data))
	end
	if not success or e and e ~= '' then return {Error = e} end
	-- 5.: tostring actual result
	if type(result.actual) == 'table' and result.actual.toString then
		result.actual = result.actual:toString()
	elseif type(result.actual) ~= 'string' then
		local _, ra = pcall(tostring, result.actual)
		if ra then result.actual = ra
		else result.actual = mw.dumpObject(result.actual) end
	end
	-- 6.: get dif
	mw.logObject(result, 'client at get dif')
	if not result.expected then
		
	elseif result.actual == result.expected then
		result.accordance1 = result.actual
	else
		local aList = mw.text.split(result.actual, '')
		local eList = mw.text.split(result.expected, '')
		local i = 1
		result.accordance1 = ""
		while i <= #aList and i <= #eList and aList[i] == eList[i] do
			result.accordance1 = result.accordance1 .. aList[i]
			i = i + 1
		end
		result.accordance1 = result.accordance1:gsub('&', '&amp;')
		local ia = #aList
		local ie = #eList
		result.accordance2 = ""
		while i <= ia and i <= ie and aList[ia] == eList[ie] do
			result.accordance2 = aList[ia] .. result.accordance2
			ia = ia - 1
			ie = ie - 1
		end
		result.accordance2 = result.accordance2:gsub('&', '&amp;')
		if i > ia then
			result.variation = '[..]'
		else
			result.variation = ''
			while i <= ia do
				result.variation = aList[ia] .. result.variation
				ia = ia - 1
			end
		end
		result.variation = result.variation:gsub('&', '&amp;')
		result.missedMark = ''
		while i <= ie do
			result.missedMark = eList[ie] .. result.missedMark
			ie = ie - 1
		end
		result.missedMark = result.missedMark:gsub('&', '&amp;')
	end
	setmetatable(result, self)
	self.__index = self
	return result
end

p.single = function(frame)
	if not frame.args then
		return	'First usage: ' .. frame:extensionTag('syntaxhighlight',
			'{{#invoke:Test|single|action=<module name>/<exported function>|...'
		..	'}}', { lang="html+handlebars"}) .. ' with whatever params your fun'
			..	'ction might need in addition'
	end
	local client = Client:new(frame)
	if client.Error then return client.Error end
	local result =	'<table class="wikitable"><tr><th>call</th><td>'
				..	frame:extensionTag('code', client.call:toString(), {})
				..	'</td></tr><tr><th>actual value</th><td>'
				..	client.actual .. '</td></tr>'
	if not client.expected then
		local call = Call:suggester(frame, 'single', client.actual)
		return	result .. '<tr><td colspan="2">following call whould indica'
			..	'te success:</td></tr><tr><td colspan="2">'
			..	frame:extensionTag('syntaxhighlight', call.wikitext, {
					lang="html+handlebars"
				}) .. '</td></tr></table>'
	end
	if client.variation then
		return	result .. '<tr><th>expected value></th><td>' ..	client.expected
			..	'</td></tr><tr><td colspan="2">' ..	client:getVariation()
			..	'</td></tr><tr><td colspan="2">'
			..	client:getMissedMark() .. '</td></tr></table>'
	end
	return result .. '</table>'
end

p.testRows = function(frame)
	if not frame.args then
		return	'First usage: ' .. frame:extensionTag('syntaxhighlight',
			[[{|
{{#invoke:Test|testRows|action=<module name>/<exported function>|...}}
|}]], { lang="html+handlebars"})
			.. ' with whatever params your function might need in addition'
	end
	local client = Client:new(frame)
	if client.Error then return '<tr><td>' .. client.Error .. '</td></tr>' end
	return client:reducedRows('testRows')
end

p.testTable = function(frame)
	
end

p.delimitLogs = function()
	mw.log("___________________________ next test ___________________________")
end

return p