mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Scribunto
synced 2024-12-01 11:26:17 +00:00
e66ccb89b4
It's easy to forget a 'local' somewhere and accidentally leak a global variable. Add a unit test to catch that. Change-Id: I3a8dda22f108d88039f9562a1da7a739850bb14b
208 lines
5.4 KiB
Lua
208 lines
5.4 KiB
Lua
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
|
|
|
|
---- 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
|