mediawiki-extensions-Scribunto/tests/engines/LuaCommon/TestFramework.lua
Brad Jorsch c84d699e9b Refactor unit tests
The existing unit tests work, but the setup is really not amenable to
the addition of additional tests in a modular fashion. This splits
things out so there is a framework for tests in Lua, and all a module
has to do on the Lua side is supply a list of functions to call and
results to expect. And then on the php side, only one array entry and
two short functions need to be added to LuaSandboxEngineTest to run the
tests.

Change-Id: Ib241b246aa0c7223c33887b38a5858582d7d31b0
2013-01-09 15:54:25 +00:00

208 lines
5.3 KiB
Lua

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
---- 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
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
}
-- 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