mediawiki-extensions-Scribunto/tests/engines/LuaCommon/TestFramework.lua

247 lines
6.3 KiB
Lua
Raw Normal View History

local testframework = testframework or {}
-- Return a string represetation of a value, including the deep structure of a table
local function deepToString( val, indent, done )
done = done or {}
indent = indent or 0
local tp = type( val )
if tp == 'string' then
return string.format( "%q", val )
elseif tp == 'table' then
if done[val] then return '{ ... }' end
done[val] = true
local sb = { '{\n' }
local donekeys = {}
for key, value in ipairs( val ) do
donekeys[key] = true
sb[#sb + 1] = string.rep( " ", indent + 2 )
sb[#sb + 1] = deepToString( value, indent + 2, done )
sb[#sb + 1] = ",\n"
end
local keys = {}
for key in pairs( val ) do
if not donekeys[key] then
keys[#keys + 1] = key
end
end
table.sort( keys )
for i = 1, #keys do
local key = keys[i]
sb[#sb + 1] = string.rep( " ", indent + 2 )
if type( key ) == 'table' then
sb[#sb + 1] = '[{ ... }] = '
else
sb[#sb + 1] = '['
sb[#sb + 1] = deepToString( key, indent + 3, done )
sb[#sb + 1] = '] = '
end
sb[#sb + 1] = deepToString( val[key], indent + 2, done )
sb[#sb + 1] = ",\n"
end
sb[#sb + 1] = string.rep( " ", indent )
sb[#sb + 1] = "}"
return table.concat( sb )
else
return tostring( val )
end
end
testframework.deepToString = deepToString
-- Test whether two objects are equal, including the deep structure of a table.
-- Returns 4 values:
-- boolean equal?
-- list key path to first inequality
-- mixed value from 'a' for key path
-- mixed value from 'b' for key path
local function deepEquals( a, b, keypath, done )
-- Simple equality
if a == b then
return true
end
keypath = keypath or {}
done = done or {}
-- Must be equal types to be equal
local tp = type( a )
if type( b ) ~= tp then
return false, keypath, a, b
end
-- Special tests for certain types
if tp == 'number' then
-- For test framework purposes, NaNs are equivalent. Lua has no
-- standard "isNaN" function, but only NaN will return true for
-- "x ~= x".
if a ~= a and b ~= b then
return true
end
return false, keypath, a, b
end
if tp == 'table' then
-- To avoid recursion, see if we've seen this pair of tables before. If
-- so, they must be equal or the test would have failed the first time we saw them.
done[a] = done[a] or {}
done[b] = done[b] or {}
if done[a][b] or done[b][a] then
return true
end
-- Not seen before, record them and compare key by key.
done[a][b] = true
local n = #keypath + 1
-- First, check if the values for all keys in 'a' are equal in 'b'.
for k in pairs( a ) do
keypath[n] = k
local ok, kp, aa, bb = deepEquals( a[k], b[k], keypath, done )
if not ok then
return false, kp, aa, bb
end
end
keypath[n] = nil
-- Then check if there are any keys in 'b' that don't exist in 'a'.
for k, v in pairs( b ) do
if a[k] == nil then
keypath[n] = k
return false, keypath, nil, v
end
end
-- Ok, all keys equal so it must match.
return true
end
-- Ok, they're not equal
return false, keypath, a, b
end
testframework.deepEquals = deepEquals
-- Skip a test (throws an error)
function testframework.markTestSkipped( message )
error( 'SKIP: ' .. message, 0 )
end
---- Test types available ---
-- Each type has a formatter and an executor:
-- Formatters take 1 arg: expected return value from the function.
-- Executors take 2 args: function and arguments.
-- Both return a string. The test passes if the two strings match.
testframework.types = testframework.types or {}
-- Execute a function and assert expected results
-- Expected value is a list of return values, or a string error message
testframework.types.Normal = {
format = function ( expect )
if type( expect ) == 'string' then
return 'ERROR: ' .. expect
else
return deepToString( expect )
end
end,
exec = function ( func, args )
local got = { pcall( func, unpack( args ) ) }
if table.remove( got, 1 ) then
return deepToString( got )
else
if string.sub( got[1], 1, 6 ) == 'SKIP: ' then
error( got[1], 0 )
end
got = string.gsub( got[1], '^%S+:%d+: ', '' )
return 'ERROR: ' .. got
end
end
}
-- Execute an iterator-returning function and assert expected results from each
-- iteration.
-- Expected value is a list of return value lists.
testframework.types.Iterator = {
format = function ( expect )
local sb = {}
for i = 1, #expect do
sb[i] = '[iteration ' .. i .. ']:\n' .. deepToString( expect[i] )
end
return table.concat( sb, '\n\n' )
end,
exec = function ( func, args )
local sb = {}
local i = 0
local f, s, var = func( unpack( args ) )
while true do
local got = { f( s, var ) }
var = got[1]
if var == nil then break end
i = i + 1
sb[i] = '[iteration ' .. i .. ']:\n' .. deepToString( got )
end
return table.concat( sb, '\n\n' )
end
}
-- Execute a function and assert expected results
-- Expected value is a list of return values, or a string error message
testframework.types.ToString = {
format = function ( expect )
if type( expect ) == 'string' then
return 'ERROR: ' .. expect
else
local ret = {}
for k, v in pairs( expect ) do
ret[k] = tostring( v )
end
return deepToString( ret )
end
end,
exec = function ( func, args )
local got = { pcall( func, unpack( args ) ) }
if table.remove( got, 1 ) then
for k, v in pairs( got ) do
got[k] = tostring( v )
end
return deepToString( got )
else
if string.sub( got[1], 1, 6 ) == 'SKIP: ' then
error( got[1], 0 )
end
got = string.gsub( got[1], '^%S+:%d+: ', '' )
return 'ERROR: ' .. got
end
end
}
-- This takes a list of tests to run, and returns the object used by PHP to
-- call them.
--
-- Each test is a table with the following keys:
-- name: Name of the test
-- expect: Table of results expected
-- func: Function to execute
-- args: (optional) Table of args to be unpacked and passed to the function
-- type: (optional) Formatter/Executor name, default "Normal"
function testframework.getTestProvider( tests )
return {
count = #tests,
provide = function ( n )
local t = tests[n]
return n, t.name, testframework.types[t.type or 'Normal'].format( t.expect )
end,
run = function ( n )
local t = tests[n]
if not t then
return 'Test ' .. name .. ' does not exist'
end
return testframework.types[t.type or 'Normal'].exec( t.func, t.args or {} )
end,
}
end
return testframework