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 pairs(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.frame:extensionTag('span',
this.call.frame:extensionTag('wikitext', result, {}),
{ style="background-color:#bfa;" })
end
else result = '' end
result = result
.. this.call.frame:extensionTag('span',
this.call.frame:extensionTag('wikitext', this.variation, {}),
{ style="background-color:#fba;" })
if not this.accordance2 or this.accordance2 == '' then return result end
return result
.. this.call.frame:extensionTag('span',
this.call.frame:extensionTag( 'wikitext', this.accordance2, {}),
{ style="background-color:#bfa;" })
end,
getMissedMark = function(this)
if not this.missedMark or this.missedMark == '' then
return this.call.frame:extensionTag('span',
this.call.frame:extensionTag('wikitext', 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( 'wikitext', result, {}),
{ style="background-color:#bfa;" })
end
else result = '' end
result = result
.. this.call.frame:extensionTag('wikitext', this.missedMark, {})
if not this.accordance2 or this.accordance2 == '' then return result end
return result
.. this.call.frame:extensionTag('span',
this.call.frame: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:getVariation()
.. '</td></tr><tr><td colspan="2">'
.. client:getMissedMark() .. '</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('&', '&')
.. ' }}', {})
.. '</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('&', '&'), {})
.. '</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('&', '&'), {})
.. '</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