local testframework = require 'Module:TestFramework' local test = {} function test.clone1() local x = 1 local y = mw.clone( x ) return ( x == y ) end function test.clone2() local x = { 'a' } local y = mw.clone( x ) assert( x ~= y ) return testframework.deepEquals( x, y ) end function test.clone2b() local x = { 'a' } local y = mw.clone( x ) assert( x ~= y ) y[2] = 'b' return testframework.deepEquals( x, y ) end function test.clone3() local mt = { __add = function() end } local x = {} setmetatable( x, mt ) local y = mw.clone( x ) assert( getmetatable( x ) ~= getmetatable( y ) ) return testframework.deepEquals( getmetatable( x ), getmetatable( y ) ) end function test.clone4() local x = {} x.x = x local y = mw.clone( x ) assert( x ~= y ) return y == y.x end function test.setfenv1() setfenv( 0, {} ) end function test.setfenv2() setfenv( 1000, {} ) end function test.setfenv3() local function jailbreak() setfenv( 2, {} ) end local new_setfenv, new_getfenv = mw.makeProtectedEnvFuncsForTest( { [_G] = true }, {} ) setfenv( jailbreak, {setfenv = new_setfenv} ) jailbreak() end function test.setfenv4() -- Set an unprotected environment at a higher stack level than a protected -- environment. It's assumed that any higher-level environment will protect -- itself with its own setfenv wrapper, so this succeeds. local function level3() local protected = {setfenv = setfenv, getfenv = getfenv, mw = mw} local function level2() local function level1() setfenv( 3, {} ) end local env = {} env.setfenv, env.getfenv = mw.makeProtectedEnvFuncsForTest( {[protected] = true}, {} ) setfenv( level1, env )() end setfenv( level2, protected )() end local unprotected = {setfenv = setfenv, getfenv = getfenv, mw = mw} setfenv( level3, unprotected )() assert( getfenv( level3 ) ~= unprotected ) return 'ok' end function test.setfenv5() local function allowed() (function() setfenv( 2, {} ) end )() end local new_setfenv, new_getfenv = mw.makeProtectedEnvFuncsForTest( { [_G] = true }, {} ) setfenv( allowed, {setfenv = new_setfenv} )() return 'ok' end function test.setfenv6() local function target() end local function jailbreak() setfenv( target, {} ) end local new_setfenv, new_getfenv = mw.makeProtectedEnvFuncsForTest( {}, { [target] = true } ) setfenv( jailbreak, {setfenv = new_setfenv} )() end function test.setfenv7() setfenv( {}, {} ) end function test.getfenv1() assert( getfenv( 1 ) == _G ) return 'ok' end function test.getfenv2() getfenv( 0 ) end function test.getfenv3() local function foo() return getfenv( 2 ) end local function bar() return foo() end -- The "at level #" bit varies between environments, so -- catch the error and strip that part out local ok, err = pcall( bar ) if not ok then err = string.gsub( err, '^%S+:%d+: ', '' ) err = string.gsub( err, ' at level %d$', '' ) error( err ) end end function test.executeExpensiveCalls( n ) for i = 1, n do mw.incrementExpensiveFunctionCount() end return 'Did not error out' end function test.stringMetatableHidden1() return getmetatable( "" ) end function test.stringMetatableHidden2() string.foo = 42 return ("").foo end local pairs_test_table = {} setmetatable( pairs_test_table, { __pairs = function () return 1, 2, 3, 'ignore' end, __ipairs = function () return 4, 5, 6, 'ignore' end, } ) function test.noLeaksViaPackageLoaded() assert( package.loaded.debug == debug, "package.loaded.debug ~= debug" ) assert( package.loaded.string == string, "package.loaded.string ~= string" ) assert( package.loaded.math == math, "package.loaded.math ~= math" ) assert( package.loaded.io == io, "package.loaded.io ~= io" ) assert( package.loaded.os == os, "package.loaded.os ~= os" ) assert( package.loaded.table == table, "package.loaded.table ~= table" ) assert( package.loaded._G == _G , "package.loaded._G ~= _G " ) assert( package.loaded.coroutine == coroutine, "package.loaded.coroutine ~= coroutine" ) assert( package.loaded.package == package, "package.loaded.package ~= package" ) return 'ok' end function test.strictGood() require( 'strict' ) local foo = "bar" return foo end function test.strictBad1() require( 'strict' ) return bar end function test.strictBad2() require( 'strict' ) bar = "foo" end test.loadData = {} function test.loadData.get( ... ) local d = mw.loadData( 'Module:CommonTests-data' ) for i = 1, select( '#', ... ) do local k = select( i, ... ) d = d[k] end return d end function test.loadData.set( v, ... ) local d = mw.loadData( 'Module:CommonTests-data' ) local n = select( '#', ... ) for i = 1, n - 1 do local k = select( i, ... ) d = d[k] end d[select( n, ... )] = v return d[select( n, ... )] end function test.loadData.recursion() local d = mw.loadData( 'Module:CommonTests-data' ) return d == d.t, d.t == d.t.t, d.table2 == d.table end function test.loadData.iterate( func ) local d = mw.loadData( 'Module:CommonTests-data' ) local ret = {} for k, v in func( d.table ) do ret[k] = v end return ret end function test.loadData.setmetatable() local d = mw.loadData( 'Module:CommonTests-data' ) setmetatable( d, {} ) return 'setmetatable succeeded' end function test.loadData.rawset() -- We can't easily prevent rawset (and it's not worth trying to redefine -- it), but we can make sure it doesn't affect other instances of the data local d1 = mw.loadData( 'Module:CommonTests-data' ) local d2 = mw.loadData( 'Module:CommonTests-data' ) rawset( d1, 'str', 'ugh' ) local d3 = mw.loadData( 'Module:CommonTests-data' ) return d1.str, d2.str, d3.str end test.loadJsonData = {} function test.loadJsonData.get( ... ) local d = mw.loadJsonData( 'Module:CommonTests-data.json' ) for i = 1, select( '#', ... ) do local k = select( i, ... ) d = d[k] end return d end function test.loadJsonData.set( v, ... ) local d = mw.loadJsonData( 'Module:CommonTests-data.json' ) local n = select( '#', ... ) for i = 1, n - 1 do local k = select( i, ... ) d = d[k] end d[select( n, ... )] = v return d[select( n, ... )] end function test.loadJsonData.iterate( func ) local d = mw.loadJsonData( 'Module:CommonTests-data.json' ) local ret = {} for k, v in func( d.table ) do ret[k] = v end return ret end function test.loadJsonData.setmetatable() local d = mw.loadJsonData( 'Module:CommonTests-data.json' ) setmetatable( d, {} ) return 'setmetatable succeeded' end function test.loadJsonData.rawset() -- We can't easily prevent rawset (and it's not worth trying to redefine -- it), but we can make sure it doesn't affect other instances of the data local d1 = mw.loadJsonData( 'Module:CommonTests-data.json' ) local d2 = mw.loadJsonData( 'Module:CommonTests-data.json' ) rawset( d1, 'str', 'ugh' ) local d3 = mw.loadJsonData( 'Module:CommonTests-data.json' ) return d1.str, d2.str, d3.str end function test.loadJsonData.error( name ) mw.loadJsonData( name ) end return testframework.getTestProvider( { { name = 'clone', func = test.clone1, expect = { true }, }, { name = 'clone table', func = test.clone2, expect = { true }, }, { name = 'clone table then modify', func = test.clone2b, expect = { false, { 2 }, nil, 'b' }, }, { name = 'clone table with metatable', func = test.clone3, expect = { true }, }, { name = 'clone recursive table', func = test.clone4, expect = { true }, }, { name = 'setfenv global', func = test.setfenv1, expect = "'setfenv' cannot set the global environment, it is protected", }, { name = 'setfenv invalid level', func = test.setfenv2, expect = "'setfenv' cannot set an environment at a level greater than 10", }, { name = 'setfenv invalid environment', func = test.setfenv3, expect = "'setfenv' cannot set the requested environment, it is protected", }, { name = 'setfenv on unprotected past protected', func = test.setfenv4, expect = { 'ok' }, }, { name = 'setfenv from inside protected', func = test.setfenv5, expect = { 'ok' }, }, { name = 'setfenv protected function', func = test.setfenv6, expect = "'setfenv' cannot be called on a protected function", }, { name = 'setfenv on a non-function', func = test.setfenv7, expect = "'setfenv' can only be called with a function or integer as the first argument", }, { name = 'getfenv(1)', func = test.getfenv1, expect = { 'ok' }, }, { name = 'getfenv(0)', func = test.getfenv2, expect = "'getfenv' cannot get the global environment", }, { name = 'getfenv with tail call', func = test.getfenv3, expect = "no function environment for tail call", }, { name = 'Not quite too many expensive function calls', func = test.executeExpensiveCalls, args = { 10 }, expect = { 'Did not error out' } }, { name = 'Too many expensive function calls', func = test.executeExpensiveCalls, args = { 11 }, expect = 'too many expensive function calls' }, { name = 'string metatable is hidden', func = test.stringMetatableHidden1, expect = { nil } }, { name = 'string is not string metatable', func = test.stringMetatableHidden2, expect = { nil } }, { name = 'pairs with __pairs', func = pairs, args = { pairs_test_table }, expect = { 1, 2, 3 }, }, { name = 'ipairs with __ipairs', func = ipairs, args = { pairs_test_table }, expect = { 4, 5, 6 }, }, { name = 'package.loaded does not leak references to out-of-environment objects', func = test.noLeaksViaPackageLoaded, expect = { 'ok' }, }, { name = 'strict on good code raises no errors', func = test.strictGood, expect = { 'bar' }, }, { name = 'strict on code reading from a global errors', func = test.strictBad1, expect = "variable 'bar' is not declared", }, { name = 'strict on code setting from a global errors', func = test.strictBad2, expect = "assign to undeclared variable 'bar'", }, { name = 'mw.loadData, returning non-table', func = mw.loadData, args = { 'Module:CommonTests-data-fail1' }, expect = "Module:CommonTests-data-fail1 returned string, table expected", }, { name = 'mw.loadData, containing function', func = mw.loadData, args = { 'Module:CommonTests-data-fail2' }, expect = "data for mw.loadData contains unsupported data type 'function'", }, { name = 'mw.loadData, containing table-with-metatable', func = mw.loadData, args = { 'Module:CommonTests-data-fail3' }, expect = "data for mw.loadData contains a table with a metatable", }, { name = 'mw.loadData, containing function as key', func = mw.loadData, args = { 'Module:CommonTests-data-fail4' }, expect = "data for mw.loadData contains unsupported data type 'function'", }, { name = 'mw.loadData, containing table-with-metatable as key', func = mw.loadData, args = { 'Module:CommonTests-data-fail5' }, expect = "data for mw.loadData contains a table as a key", }, { name = 'mw.loadData, getter (true)', func = test.loadData.get, args = { 'true' }, expect = { true } }, { name = 'mw.loadData, getter (false)', func = test.loadData.get, args = { 'false' }, expect = { false } }, { name = 'mw.loadData, getter (NaN)', func = test.loadData.get, args = { 'NaN' }, expect = { 0/0 } }, { name = 'mw.loadData, getter (inf)', func = test.loadData.get, args = { 'inf' }, expect = { 1/0 } }, { name = 'mw.loadData, getter (num)', func = test.loadData.get, args = { 'num' }, expect = { 12.5 } }, { name = 'mw.loadData, getter (str)', func = test.loadData.get, args = { 'str' }, expect = { 'foo bar' } }, { name = 'mw.loadData, getter (table.2)', func = test.loadData.get, args = { 'table', 2 }, expect = { 'two' } }, { name = 'mw.loadData, getter (t.t.t.t.str)', func = test.loadData.get, args = { 't', 't', 't', 't', 'str' }, expect = { 'foo bar' } }, { name = 'mw.loadData, getter recursion', func = test.loadData.recursion, expect = { true, true, true }, }, { name = 'mw.loadData, pairs', func = test.loadData.iterate, args = { pairs }, expect = { { 'one', 'two', 'three', foo = 'bar' } }, }, { name = 'mw.loadData, ipairs', func = test.loadData.iterate, args = { ipairs }, expect = { { 'one', 'two', 'three' } }, }, { name = 'mw.loadData, setmetatable', func = test.loadData.setmetatable, expect = "cannot change a protected metatable" }, { name = 'mw.loadData, setter (1)', func = test.loadData.set, args = { 'ugh', 'str' }, expect = "table from mw.loadData is read-only", }, { name = 'mw.loadData, setter (2)', func = test.loadData.set, args = { 'ugh', 'table', 2 }, expect = "table from mw.loadData is read-only", }, { name = 'mw.loadData, setter (3)', func = test.loadData.set, args = { 'ugh', 't' }, expect = "table from mw.loadData is read-only", }, { name = 'mw.loadData, rawset', func = test.loadData.rawset, expect = { 'ugh', 'foo bar', 'foo bar' }, }, { name = 'mw.loadJsonData, getter (true)', func = test.loadJsonData.get, args = { 'true' }, expect = { true } }, { name = 'mw.loadJsonData, getter (false)', func = test.loadJsonData.get, args = { 'false' }, expect = { false } }, { name = 'mw.loadJsonData, getter (num)', func = test.loadJsonData.get, args = { 'num' }, expect = { 12.5 } }, { name = 'mw.loadJsonData, getter (str)', func = test.loadJsonData.get, args = { 'str' }, expect = { 'foo bar' } }, { name = 'mw.loadJsonData, getter (table.2)', func = test.loadJsonData.get, args = { 'table', 2 }, expect = { 'two' } }, { name = 'mw.loadJsonData, pairs', func = test.loadJsonData.iterate, args = { pairs }, expect = { { 'one', 'two', 'three' } }, }, { name = 'mw.loadJsonData, ipairs', func = test.loadJsonData.iterate, args = { ipairs }, expect = { { 'one', 'two', 'three' } }, }, { name = 'mw.loadJsonData, setmetatable', func = test.loadJsonData.setmetatable, expect = "cannot change a protected metatable" }, { name = 'mw.loadJsonData, setter (1)', func = test.loadJsonData.set, args = { 'ugh', 'str' }, expect = "table from mw.loadJsonData is read-only", }, { name = 'mw.loadJsonData, setter (2)', func = test.loadJsonData.set, args = { 'ugh', 'table', 2 }, expect = "table from mw.loadJsonData is read-only", }, { name = 'mw.loadJsonData, setter (3)', func = test.loadJsonData.set, args = { 'ugh', 't' }, expect = "table from mw.loadJsonData is read-only", }, { name = 'mw.loadJsonData, rawset', func = test.loadJsonData.rawset, expect = { 'ugh', 'foo bar', 'foo bar' }, }, { name = 'mw.loadJsonData, bad title (1)', func = test.loadJsonData.error, args = { 0 }, expect = "bad argument #1 to 'mw.loadJsonData' (string expected, got nil)", }, { name = 'mw.loadJsonData, bad title (2)', func = test.loadJsonData.error, args = { "" }, expect = "bad argument #1 to 'mw.loadJsonData' ('' is not a valid JSON page)", }, { name = 'mw.loadJsonData, bad title (3)', func = test.loadJsonData.error, args = { "Help:Foo" }, expect = "bad argument #1 to 'mw.loadJsonData' ('Help:Foo' is not a valid JSON page)", }, { name = 'mw.loadJsonData, bad title (4)', func = test.loadJsonData.error, args = { "Help:Does not exist" }, expect = "bad argument #1 to 'mw.loadJsonData' ('Help:Does not exist' is not a valid JSON page)", }, { name = 'mw.addWarning', func = mw.addWarning, args = { 'warn' }, expect = {}, }, { name = 'mw.addWarning, bad type', func = mw.addWarning, args = { true }, expect = "bad argument #1 to 'addWarning' (string expected)", }, } )