Modul:Test

Dies ist eine alte Version dieser Seite, zuletzt bearbeitet am 23. Dezember 2022 um 03:09 Uhr durch Vollbracht (Diskussion | Beiträge). Sie kann sich erheblich von der aktuellen Version unterscheiden.
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>{!} ... }}
]=]

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)						an invoke call of a Test function
		invoker(frame, action, data)	testcase callable by invoke
		requirer(frame, action, data)	testcase callable by require
		suggester(frame, test, result)	an invoke call of a Test function
	fields:
		wikitext		string	available for invoke only
		luatext			string
		modulename		string
		moduleprefix	string	'Module' or 'Modul'
		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()
		getModule()		returns required module by this name
		extensionTag(element, core, attributes) copy of frames extensionTag
]]
local Call = {
	properties = {module=1, func=2, expected=4, unnamed=8, export=16},
	process = function(this)
		local c = this.client
		if c.err then
			return "Didn't process due to previous error:\n" .. this.err
		end
		errHandler = function(runtimeError)
			this.client.err = runtimeError
		end
		caller = function()
			ps = c.paramset
			return c.func(unpack(ps))
		end
		local success, value, err = xpcall(caller, errHandler)
		if success then
			if not value then
				mw.logObject({
					Success = success,
					Error1 = this.client.err,
					Error2 = err
				}, 'not value!')
				mw.logObject(c, 'in processing')
				value = ''
			end
		else
			mw.logObject({
				Success	= success,
				Value	= value,
				Error1	= this.client.err,
				Error2	= err
			})
			mw.logObject(c, 'in processing')
			return c.wraper .. ' with error: ' .. this.client.err
		end
		this.actualValue = value
		return value
	end,
	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,
	getModule = function(this)
		local name = this.modulename
		if not name or name == '' then return nil end
		if	this.wikitext and
			(not this.moduleprefix or this.moduleprefix == '') then
			local _, result = pcall(require, 'Module:' .. name)
			if not result then _, result = pcall(require, 'Modul:' .. name) end
			return result
		end
		if not this.moduleprefix or this.moduleprefix == '' then return nil end
		local _, result = pcall(require, this.moduleprefix .. ':' .. name)
		return result
	end,
	toCode = function(this)
		return this.frame:extensionTag('code', this:toString(), {})
	end,
	setModule = function(this, name)
		if this.err then
			mw.log("Didn't set module due to previous error:\n" .. this.err)
			return this
		end
		local success = ''
		success, this.Module = pcall(require, name)
		if not success or not this.Module then
			this.err = 'Failed to access module ' .. name
			if not name:find('Modul[e]?%:') then
				this.err =	this.err
						..	'. Try providing a "Module:" or "Modul:" prefix!'
			end
			mw.log(this.err)
			mw.log(success)
		end
	end,
	getExport = function(this, name)
		if this.err then
			mw.log("Didn't return export due to previous error:\n" .. this.err)
			return nil
		end
		if name == "" then return this.Module end
		local e = this.Module[name]
		if not e then
			this.err =	"This module doesn't provide an export " ..	name
					..	'\n<code>' .. result.wraper .. '</code> will fail!'
			mw.log(err)
			return nil
		end
		return e
	end,
	setFunction = function(this, provider, cModule, name, cExport)
		if this.err then
			mw.log("Didn't set function due to previous error:\n" .. this.err)
			return this
		end
		if provider and provider[name] then this.func = provider[name]
		else
			this.err =	cModule .. " doesn't provide a function "
			if cExport and cExport ~= "" then
				this.err = this.err .. cExport .. '.'
			end
			this.err =	this.err .. name .. '\n'
					..	frame:extensionTag('nowiki', this.wraper, {})
					..	' will fail!'
			mw.log(err)
		end
		return this
	end,
}

function Call:Iclient(frame, cModule, cFunc, cParams)
	local result = {
		params = cParams, scheme = 0,
		paramset = {frame:newChild({title = cModule, args=cParams})}
	}
	local modName = cModule:match('Modul[e]?:(.*)')
	if not modName then modName = cModule end
	result.wraper =	'{{#invoke:' .. modName ..	' |' .. cFunc .. ' |'
	for k, v in pairs(cParams) do
		result.wraper = result.wraper .. k .. '=' .. v .. ' |'
	end
	for _, v in ipairs(cParams) do
		result.wraper = result.wraper .. v .. '|'
	end
	local w = result.wraper
	result.wraper = result.wraper:sub(1, #w - 2) .. '}}'
	result.client = result
	setmetatable(result, self)
	self.__index = self
	result:setModule(cModule)
	return result
end

function Call:Rclient(cModule, cFunc, cParams, cExport)
	local result = {
		params = cParams, scheme = 0, paramset = cParams,
		wraper = cModule .. ' provided '
	}
	if cExport and cExport ~= "" then
		result.wraper = result.wraper .. cExport .. '.' .. cFunc .. '('
	else
		result.wraper = result.wraper .. cFunc .. '('
	end
	for _, v in ipairs(cParams) do
		result.wraper = result.wraper .. v .. ', '
	end
	local w = result.wraper
	result.wraper = result.wraper:sub(1, #w - 2) .. ')'
	result.client = result
	setmetatable(result, self)
	self.__index = self
	result:setModule(cModule)
	return result
end

--[[
	invoker(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
		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:invoker(frame, action, data)
	if not frame or not frame.extensionTag or not action then return nil end
	if type(action) == 'string' then
		action = mw.text.split(action, '$s*%/%s*')
	end
	if type(action) ~= 'table' or #action < 2 then return nil end
	local result = {
		frame = frame,
		functionpath = { action[2] },
		parameters = data
	}
	result.moduleprefix, result.modulename = action[1]:match('(Modul[e]?):(.*)')
	if not result.modulename then result.modulename = action[1] 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 = action[2] .. '()'
		result.wikitext =	'{{#invoke:' .. result.modulename .. ' |'
						..	action[2] .. '}}'
	else
		result.luatext = action[2] .. '{' .. luadata:sub(3) .. '}'
		result.wikitext =	'{{#invoke:' .. result.modulename .. ' |'
						..	action[2] .. wtData:gsub('%s*$', '') .. '}}'
	end
	setmetatable(result, self)
	self.__index = self
	return result
end

--[[
	requirer(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
		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:requirer(frame, action, data)
	if not frame or not frame.extensionTag or not action then return nil end
	if type(action) == 'string' then
		action = mw.text.split(action, '$s*%/%s*')
	end
	if type(action) ~= 'table' or #action < 2 then return nil end
	local result = {
		frame = frame,
		functionpath = {},
		parameters = data,
		wikitext = nil
	}
	for i = 2, #action do table.insert(result.functionpath, action[i]) end
	result.moduleprefix, result.modulename = action[1]:match('(Modul[e]?):(.*)')
	if not result.modulename then result.modulename = action[1] end
	local luadata = ''
	for i, v in ipairs(data) do
		luadata = luadata .. ', [' .. i .. '] = "' .. v .. '"'
	end
	for k, v in ipairs(data) do
		luadata = luadata .. ', ' .. k .. ' = "' .. v .. '"'
	end
	if luadata == '' then
		result.luatext = action[2] .. '()'
	else
		result.luatext = action[2] .. '{' .. luadata:sub(3) .. '}'
	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 ipairs(frame.args) do
		luadata = luadata .. ', ' .. k .. ' = "' .. v .. '"'
		wtData = wtData .. '|' .. k .. '=' .. v .. ' '
	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

function Call:new(frame, test)
	local result = {
		paramset = {frame}, scheme = 0,
		wikitext='{{#invoke:Test|' .. test .. ' |'
	}
	local clientParams = {}
	for k, v in pairs(frame.args) do
		local t = Call.properties[k]
		if t then
			result[k] = v
			result.scheme = result.scheme + t
		else clientParams[k] = v end
		result.wikitext = result.wikitext .. k .. '=' .. v .. ' |'
	end
	if result.scheme % 4 < 3 then return nil end
	for _, v in ipairs(frame.args) do
		table.insert(clientParams, v)
		result.wikitext = result.wikitext .. v .. ' |'
	end
	result.wikitext = result.wikitext:sub(1, #result.wikitext-2) .. '}}'
	setmetatable(result, self)
	self.__index = self
	if result.export then
		result.client = Call:Rclient(	result.module, result.func,
										clientParams, result.export)
		local e = result.client:getExport(result.export)
		result.client:setFunction(e, result.module, result.func, result.export)
	else
		result.client = Call:Iclient(	frame, result.module, result.func,
										clientParams)
		if result.client.err then
			result.client = Call:Rclient(	result.module, result.func,
											clientParams)
		end
		if result.client.err then
			result.client = Call:Rclient(	result.module, result.func,
											clientParams, 'service')
			local e = result.client:getExport('service')
			result.client:setFunction(e, result.module, result.func, 'service')
		else
			result.client:setFunction(	result.client.Module,
										result.module, result.func)
		end
	end
	if result.client.err and not result.err then
		result.err = result.client.err
	end
	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:extensionTag('span',
							this.call:extensionTag(	'wikitext', result, {}),
							{ style="background-color:#bfa;" })
			end
		else result = '' end
		result =	result
				..	this.call:extensionTag('span',
						this.call:extensionTag('wikitext', this.variation, {}),
						{ style="background-color:#fba;" })
		if not this.accordance2 or this.accordance2 == '' then return result end
		return	result
			..	this.call:extensionTag('span',
					this.call:extensionTag(	'wikitext', this.accordance2, {}),
					{ style="background-color:#bfa;" })
	end,
	getMissedMark = function(this)
		if not this.missedMark or this.missedMark == '' then
			return	this.call:extensionTag('span',
						this.call:extensionTag('wikitext', this.expected, {}),
						{ style="background-color:#bfa;" })
		end
		local result = this.accordance1
		if result then
			if result ~= '' then
				result = this.call:extensionTag('span',
							this.call:extensionTag(	'wikitext', result, {}),
							{ style="background-color:#bfa;" })
			end
		else result = '' end
		result =	result
				..	this.call:extensionTag('wikitext', this.missedMark, {})
		if not this.accordance2 or this.accordance2 == '' then return result end
		return	result
			..	this.call:extensionTag('span',
					this.call:extensionTag(	'wikitext', this.accordance2, {}),
					{ style="background-color:#bfa;" })
	end,
	toString = function(this) -- ####
		if this.variation then return this:getVariation() end
		return this.actual
	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
		end
		if v then data[k] = v end
	end
	if not action then
		return {
			Error = 'Missing argument "action=<module name>/<function name>".'
		}
	end
	action = mw.text.split(action, '%/')
	-- 2.: generate string representation from action
	local e = ""
	if #action > 2 then
		result.call = Call:requirer(frame, action, data)
	elseif #action == 2 then
		result.call = Call:invoker(frame, action, data)
	else
		e =	'Inappropriate argument "action" is "' .. action[1]
		..	'" 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:getModule()
	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
		handler = function(err)
			e = err
			mw.logObject(e, 'tostring error in result of '
						..	action[#action] .. ' function')
		end
		fkt = function()
			return tostring(result.actual)
		end
		success, result.actual = xpcall(fkt, handler, {})
		if not success or e and e ~= '' then return {Error = e} end
	end
	-- 6.: 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
		local ia = #aList
		local ie = #eList
		result.accordance2 = ""
		while i <= ia and i <= ie and aList[ia] == eList[ie] do
			result.accordance1 = aList[ia] .. result.accordance2
			ia = ia - 1
			ie = ie - 1
		end
		if i <= ie then
			result.missedMark = result.expected:sub(i, ie)
			if i <= ia then result.variation = result.actual:sub(i, ia)
			else result.variation = '[..]' end
		else
			result.variation = result.actual:sub(i, ia)
		end
	end
	setmetatable(result, self)
	self.__index = self
	return result
end

p.single2 = 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, 'single2', 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.getField('variation')
			..	'</td></tr><tr><td colspan="2">'
			..	client.getField('missedMark') .. '</td></tr></table>'
	end
	return result .. '</table>'
end

p.single = function(frame)
	if not frame.args then return USAGE end
	local call = Call:new(frame, 'single')
	if not call then return USAGE end
	if call.err then return call.err end
	local actualValue = call:process()
	if call.err then return call.err end
	local expected = call.expected
	local w = call.client.wraper
	local result =	'<table class="wikitable"><tr><th>call</th><td><code>' .. w
				..	'</code></td></tr><tr><th>actual value</th><td>'
				..	actualValue .. '</td></tr>'
	if not expected then
		w = call.wikitext -- not client wraper!
		return	result ..'<tr><td colspan="2">following call whould indicate su'
			..	'ccess:</td></tr><tr><td colspan="2">'
			..	frame:extensionTag(	'nowiki',
									w:sub(1, #w - 2) ..	'|expected='
								..	actualValue:gsub('&', '&amp;')
								.. ' }}', {})
			..	'</td></tr></table>'
	end
	if actualValue == expected then return result .. '</table>' end
	result =	result .. '<tr><th>expected value</th><td>' .. expected
			..	'</td></tr>'
	local aList = mw.text.split(actualValue, '')
	local eList = mw.text.split(expected, '')
	local i = 1
	local startS = ""
	while i < #aList and i < #eList and aList[i] == eList[i] do
		startS = startS .. aList[i]
		i = i + 1
	end
	if startS ~= "" then
		startS =	'<code style="background-color:#bfa">'
				..	frame:extensionTag('nowiki', startS, {})
				..	'</code>'
	end
	local ia = #aList
	local ie = #eList
	local endS = ""
	while i <= ia and i <= ie and aList[ia] == eList[ie] do
		endS = aList[ia] .. endS
		ia = ia - 1
		ie = ie - 1
	end
	if endS ~= "" then
		endS =	'<span style="background-color:#bfa">'
			..	frame:extensionTag('nowiki', endS, {})
			..	'</span>'
		mw.log(endS)
	end
	local errSa = ""
	if i <= ia then errSa = actualValue:sub(i, ia) end
	if errSa ~= "" then
		errSa =	'<code style="background-color:#fba">'
			..	frame:extensionTag(	'nowiki',
									errSa:gsub('&', '&amp;'), {})
			..	'</code>'
	else
		errSa =	'<code style="background-color:#fba">[__]</code>'
	end
	local errSe = ""
	if i <= ie then errSe = expected:sub(i, ie) end
	if errSe ~= "" then
		errSe =	'<code>' ..	frame:extensionTag('nowiki',
									errSe:gsub('&', '&amp;'), {})
			..	'</code>'
	end
	return result .. '<tr><td colspan="2">' .. startS .. errSa .. endS
		.. '</tr><tr><td colspan="2">' .. startS .. errSe .. endS
		.. '</td></tr></table>'
end

return p