diff options
Diffstat (limited to 'www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon')
36 files changed, 5873 insertions, 0 deletions
diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTest.php b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTest.php new file mode 100644 index 00000000..8636d302 --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTest.php @@ -0,0 +1,751 @@ +<?php + +// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps +class Scribunto_LuaCommonTest extends Scribunto_LuaEngineTestBase { + protected static $moduleName = 'CommonTests'; + + private static $allowedGlobals = [ + // Functions + 'assert', + 'error', + 'getfenv', + 'getmetatable', + 'ipairs', + 'next', + 'pairs', + 'pcall', + 'rawequal', + 'rawget', + 'rawset', + 'require', + 'select', + 'setfenv', + 'setmetatable', + 'tonumber', + 'tostring', + 'type', + 'unpack', + 'xpcall', + + // Packages + '_G', + 'debug', + 'math', + 'mw', + 'os', + 'package', + 'string', + 'table', + + // Misc + '_VERSION', + ]; + + protected function setUp() { + parent::setUp(); + + // Register libraries for self::testPHPLibrary() + $this->mergeMwGlobalArrayValue( 'wgHooks', [ + 'ScribuntoExternalLibraries' => [ + function ( $engine, &$libs ) { + $libs += [ + 'CommonTestsLib' => [ + 'class' => 'Scribunto_LuaCommonTestsLibrary', + 'deferLoad' => true, + ], + 'CommonTestsFailLib' => [ + 'class' => 'Scribunto_LuaCommonTestsFailLibrary', + 'deferLoad' => true, + ], + ]; + } + ] + ] ); + + // Note this depends on every iteration of the data provider running with a clean parser + $this->getEngine()->getParser()->getOptions()->setExpensiveParserFunctionLimit( 10 ); + + // Some of the tests need this + $interpreter = $this->getEngine()->getInterpreter(); + $interpreter->callFunction( $interpreter->loadString( + 'mw.makeProtectedEnvFuncsForTest = mw.makeProtectedEnvFuncs', 'fortest' + ) ); + } + + protected function getTestModules() { + return parent::getTestModules() + [ + 'CommonTests' => __DIR__ . '/CommonTests.lua', + 'CommonTests-data' => __DIR__ . '/CommonTests-data.lua', + 'CommonTests-data-fail1' => __DIR__ . '/CommonTests-data-fail1.lua', + 'CommonTests-data-fail2' => __DIR__ . '/CommonTests-data-fail2.lua', + 'CommonTests-data-fail3' => __DIR__ . '/CommonTests-data-fail3.lua', + 'CommonTests-data-fail4' => __DIR__ . '/CommonTests-data-fail4.lua', + 'CommonTests-data-fail5' => __DIR__ . '/CommonTests-data-fail5.lua', + ]; + } + + public function testNoLeakedGlobals() { + $interpreter = $this->getEngine()->getInterpreter(); + + list( $actualGlobals ) = $interpreter->callFunction( + $interpreter->loadString( + 'local t = {} for k in pairs( _G ) do t[#t+1] = k end return t', + 'getglobals' + ) + ); + + $leakedGlobals = array_diff( $actualGlobals, self::$allowedGlobals ); + $this->assertEquals( 0, count( $leakedGlobals ), + 'The following globals are leaked: ' . implode( ' ', $leakedGlobals ) + ); + } + + public function testPHPLibrary() { + $engine = $this->getEngine(); + $frame = $engine->getParser()->getPreprocessor()->newFrame(); + + $title = Title::makeTitle( NS_MODULE, 'TestInfoPassViaPHPLibrary' ); + $this->extraModules[$title->getFullText()] = ' + local p = {} + + function p.test() + local lib = require( "CommonTestsLib" ) + return table.concat( { lib.test() }, "; " ) + end + + function p.setVal( frame ) + local lib = require( "CommonTestsLib" ) + lib.val = frame.args[1] + lib.foobar.val = frame.args[1] + end + + function p.getVal() + local lib = require( "CommonTestsLib" ) + return tostring( lib.val ), tostring( lib.foobar.val ) + end + + function p.getSetVal( frame ) + p.setVal( frame ) + return p.getVal() + end + + function p.checkPackage() + local ret = {} + ret[1] = package.loaded["CommonTestsLib"] == nil + require( "CommonTestsLib" ) + ret[2] = package.loaded["CommonTestsLib"] ~= nil + return ret[1], ret[2] + end + + function p.libSetVal( frame ) + local lib = require( "CommonTestsLib" ) + return lib.setVal( frame ) + end + + function p.libGetVal() + local lib = require( "CommonTestsLib" ) + return lib.getVal() + end + + return p + '; + + # Test loading + $module = $engine->fetchModuleFromParser( $title ); + $ret = $module->invoke( 'test', $frame->newChild() ); + $this->assertSame( 'Test option; Test function', $ret, + 'Library can be loaded and called' ); + + # Test package.loaded + $module = $engine->fetchModuleFromParser( $title ); + $ret = $module->invoke( 'checkPackage', $frame->newChild() ); + $this->assertSame( 'truetrue', $ret, + 'package.loaded is right on the first call' ); + $ret = $module->invoke( 'checkPackage', $frame->newChild() ); + $this->assertSame( 'truetrue', $ret, + 'package.loaded is right on the second call' ); + + # Test caching for require + $args = $engine->getParser()->getPreprocessor()->newPartNodeArray( [ 1 => 'cached' ] ); + $ret = $module->invoke( 'getSetVal', $frame->newChild( $args ) ); + $this->assertSame( 'cachedcached', $ret, + 'same loaded table is returned by multiple require calls' ); + + # Test no data communication between invokes + $module = $engine->fetchModuleFromParser( $title ); + $args = $engine->getParser()->getPreprocessor()->newPartNodeArray( [ 1 => 'fail' ] ); + $module->invoke( 'setVal', $frame->newChild( $args ) ); + $ret = $module->invoke( 'getVal', $frame->newChild() ); + $this->assertSame( 'nilnope', $ret, + 'same loaded table is not shared between invokes' ); + + # Test that the library isn't being recreated between invokes + $module = $engine->fetchModuleFromParser( $title ); + $ret = $module->invoke( 'libGetVal', $frame->newChild() ); + $this->assertSame( 'nil', $ret, 'sanity check' ); + $args = $engine->getParser()->getPreprocessor()->newPartNodeArray( [ 1 => 'ok' ] ); + $module->invoke( 'libSetVal', $frame->newChild( $args ) ); + + $module = $engine->fetchModuleFromParser( $title ); + $ret = $module->invoke( 'libGetVal', $frame->newChild() ); + $this->assertSame( 'ok', $ret, + 'library is not recreated between invokes' ); + } + + public function testModuleStringExtend() { + $engine = $this->getEngine(); + $interpreter = $engine->getInterpreter(); + + $interpreter->callFunction( + $interpreter->loadString( 'string.testModuleStringExtend = "ok"', 'extendstring' ) + ); + $ret = $interpreter->callFunction( + $interpreter->loadString( 'return ("").testModuleStringExtend', 'teststring1' ) + ); + $this->assertSame( [ 'ok' ], $ret, 'string can be extended' ); + + $this->extraModules['Module:testModuleStringExtend'] = ' + return { + test = function() return ("").testModuleStringExtend end + } + '; + $module = $engine->fetchModuleFromParser( + Title::makeTitle( NS_MODULE, 'testModuleStringExtend' ) + ); + $ret = $interpreter->callFunction( + $engine->executeModule( $module->getInitChunk(), 'test', null ) + ); + $this->assertSame( [ 'ok' ], $ret, 'string extension can be used from module' ); + + $this->extraModules['Module:testModuleStringExtend2'] = ' + return { + test = function() + string.testModuleStringExtend = "fail" + return ("").testModuleStringExtend + end + } + '; + $module = $engine->fetchModuleFromParser( + Title::makeTitle( NS_MODULE, 'testModuleStringExtend2' ) + ); + $ret = $interpreter->callFunction( + $engine->executeModule( $module->getInitChunk(), 'test', null ) + ); + $this->assertSame( [ 'ok' ], $ret, 'string extension cannot be modified from module' ); + $ret = $interpreter->callFunction( + $interpreter->loadString( 'return string.testModuleStringExtend', 'teststring2' ) + ); + $this->assertSame( [ 'ok' ], $ret, 'string extension cannot be modified from module' ); + + $ret = $engine->runConsole( [ + 'prevQuestions' => [], + 'question' => '=("").testModuleStringExtend', + 'content' => 'return {}', + 'title' => Title::makeTitle( NS_MODULE, 'dummy' ), + ] ); + $this->assertSame( 'ok', $ret['return'], 'string extension can be used from console' ); + + $ret = $engine->runConsole( [ + 'prevQuestions' => [ 'string.fail = "fail"' ], + 'question' => '=("").fail', + 'content' => 'return {}', + 'title' => Title::makeTitle( NS_MODULE, 'dummy' ), + ] ); + $this->assertSame( 'nil', $ret['return'], 'string cannot be extended from console' ); + + $ret = $engine->runConsole( [ + 'prevQuestions' => [ 'string.testModuleStringExtend = "fail"' ], + 'question' => '=("").testModuleStringExtend', + 'content' => 'return {}', + 'title' => Title::makeTitle( NS_MODULE, 'dummy' ), + ] ); + $this->assertSame( 'ok', $ret['return'], 'string extension cannot be modified from console' ); + $ret = $interpreter->callFunction( + $interpreter->loadString( 'return string.testModuleStringExtend', 'teststring3' ) + ); + $this->assertSame( [ 'ok' ], $ret, 'string extension cannot be modified from console' ); + + $interpreter->callFunction( + $interpreter->loadString( 'string.testModuleStringExtend = nil', 'unextendstring' ) + ); + } + + public function testLoadDataLoadedOnce() { + $engine = $this->getEngine(); + $interpreter = $engine->getInterpreter(); + $frame = $engine->getParser()->getPreprocessor()->newFrame(); + + $loadcount = 0; + $interpreter->callFunction( + $interpreter->loadString( 'mw.markLoaded = ...', 'fortest' ), + $interpreter->wrapPHPFunction( function () use ( &$loadcount ) { + $loadcount++; + } ) + ); + $this->extraModules['Module:TestLoadDataLoadedOnce-data'] = ' + mw.markLoaded() + return {} + '; + $this->extraModules['Module:TestLoadDataLoadedOnce'] = ' + local data = mw.loadData( "Module:TestLoadDataLoadedOnce-data" ) + return { + foo = function() end, + bar = function() + return tostring( package.loaded["Module:TestLoadDataLoadedOnce-data"] ) + end, + } + '; + + // Make sure data module isn't parsed twice. Simulate several {{#invoke:}}s + $title = Title::makeTitle( NS_MODULE, 'TestLoadDataLoadedOnce' ); + for ( $i = 0; $i < 10; $i++ ) { + $module = $engine->fetchModuleFromParser( $title ); + $module->invoke( 'foo', $frame->newChild() ); + } + $this->assertSame( 1, $loadcount, 'data module was loaded more than once' ); + + // Make sure data module isn't in package.loaded + $this->assertSame( 'nil', $module->invoke( 'bar', $frame ), + 'data module was stored in module\'s package.loaded' + ); + $this->assertSame( [ 'nil' ], + $interpreter->callFunction( $interpreter->loadString( + 'return tostring( package.loaded["Module:TestLoadDataLoadedOnce-data"] )', 'getLoaded' + ) ), + 'data module was stored in top level\'s package.loaded' + ); + } + + public function testFrames() { + $engine = $this->getEngine(); + + $ret = $engine->runConsole( [ + 'prevQuestions' => [], + 'question' => '=mw.getCurrentFrame()', + 'content' => 'return {}', + 'title' => Title::makeTitle( NS_MODULE, 'dummy' ), + ] ); + $this->assertSame( 'table', $ret['return'], 'frames can be used in the console' ); + + $ret = $engine->runConsole( [ + 'prevQuestions' => [], + 'question' => '=mw.getCurrentFrame():newChild{}', + 'content' => 'return {}', + 'title' => Title::makeTitle( NS_MODULE, 'dummy' ), + ] ); + $this->assertSame( 'table', $ret['return'], 'child frames can be created' ); + + $ret = $engine->runConsole( [ + 'prevQuestions' => [ + 'f = mw.getCurrentFrame():newChild{ args = { "ok" } }', + 'f2 = f:newChild{ args = {} }' + ], + 'question' => '=f2:getParent().args[1], f2:getParent():getParent()', + 'content' => 'return {}', + 'title' => Title::makeTitle( NS_MODULE, 'dummy' ), + ] ); + $this->assertSame( "ok\ttable", $ret['return'], 'child frames have correct parents' ); + } + + public function testCallParserFunction() { + $engine = $this->getEngine(); + $parser = $engine->getParser(); + + $args = [ + 'prevQuestions' => [], + 'content' => 'return {}', + 'title' => Title::makeTitle( NS_MODULE, 'dummy' ), + ]; + + // Test argument calling conventions + $ret = $engine->runConsole( [ + 'question' => '=mw.getCurrentFrame():callParserFunction{ + name = "urlencode", args = { "x x", "wiki" } + }', + ] + $args ); + $this->assertSame( "x_x", $ret['return'], + 'callParserFunction works for {{urlencode:x x|wiki}} (named args w/table)' + ); + + $ret = $engine->runConsole( [ + 'question' => '=mw.getCurrentFrame():callParserFunction{ + name = "urlencode", args = "x x" + }', + ] + $args ); + $this->assertSame( "x+x", $ret['return'], + 'callParserFunction works for {{urlencode:x x}} (named args w/scalar)' + ); + + $ret = $engine->runConsole( [ + 'question' => '=mw.getCurrentFrame():callParserFunction( "urlencode", { "x x", "wiki" } )', + ] + $args ); + $this->assertSame( "x_x", $ret['return'], + 'callParserFunction works for {{urlencode:x x|wiki}} (positional args w/table)' + ); + + $ret = $engine->runConsole( [ + 'question' => '=mw.getCurrentFrame():callParserFunction( "urlencode", "x x", "wiki" )', + ] + $args ); + $this->assertSame( "x_x", $ret['return'], + 'callParserFunction works for {{urlencode:x x|wiki}} (positional args w/scalars)' + ); + + $ret = $engine->runConsole( [ + 'question' => '=mw.getCurrentFrame():callParserFunction{ + name = "urlencode:x x", args = { "wiki" } + }', + ] + $args ); + $this->assertSame( "x_x", $ret['return'], + 'callParserFunction works for {{urlencode:x x|wiki}} (colon in name, named args w/table)' + ); + + $ret = $engine->runConsole( [ + 'question' => '=mw.getCurrentFrame():callParserFunction{ + name = "urlencode:x x", args = "wiki" + }', + ] + $args ); + $this->assertSame( "x_x", $ret['return'], + 'callParserFunction works for {{urlencode:x x|wiki}} (colon in name, named args w/scalar)' + ); + + $ret = $engine->runConsole( [ + 'question' => '=mw.getCurrentFrame():callParserFunction( "urlencode:x x", { "wiki" } )', + ] + $args ); + $this->assertSame( "x_x", $ret['return'], + 'callParserFunction works for {{urlencode:x x|wiki}} (colon in name, positional args w/table)' + ); + + $ret = $engine->runConsole( [ + 'question' => '=mw.getCurrentFrame():callParserFunction( "urlencode:x x", "wiki" )', + ] + $args ); + $this->assertSame( "x_x", $ret['return'], + 'callParserFunction works for {{urlencode:x x|wiki}} (colon in name, positional args w/scalars)' + ); + + // Test named args to the parser function + $ret = $engine->runConsole( [ + 'question' => '=mw.getCurrentFrame():callParserFunction( "#tag:pre", + { "foo", style = "margin-left: 1.6em" } + )', + ] + $args ); + $this->assertSame( + '<pre style="margin-left: 1.6em">foo</pre>', + $parser->mStripState->unstripBoth( $ret['return'] ), + 'callParserFunction works for {{#tag:pre|foo|style=margin-left: 1.6em}}' + ); + + // Test extensionTag + $ret = $engine->runConsole( [ + 'question' => '=mw.getCurrentFrame():extensionTag( "pre", "foo", + { style = "margin-left: 1.6em" } + )', + ] + $args ); + $this->assertSame( + '<pre style="margin-left: 1.6em">foo</pre>', + $parser->mStripState->unstripBoth( $ret['return'] ), + 'extensionTag works for {{#tag:pre|foo|style=margin-left: 1.6em}}' + ); + + $ret = $engine->runConsole( [ + 'question' => '=mw.getCurrentFrame():extensionTag{ name = "pre", content = "foo", + args = { style = "margin-left: 1.6em" } + }', + ] + $args ); + $this->assertSame( + '<pre style="margin-left: 1.6em">foo</pre>', + $parser->mStripState->unstripBoth( $ret['return'] ), + 'extensionTag works for {{#tag:pre|foo|style=margin-left: 1.6em}}' + ); + + // Test calling a non-existent function + try { + $ret = $engine->runConsole( [ + 'question' => '=mw.getCurrentFrame():callParserFunction{ + name = "thisDoesNotExist", args = { "" } + }', + ] + $args ); + $this->fail( "Expected LuaError not thrown for nonexistent parser function" ); + } catch ( Scribunto_LuaError $err ) { + $this->assertSame( + 'Lua error: callParserFunction: function "thisDoesNotExist" was not found.', + $err->getMessage(), + 'callParserFunction correctly errors for nonexistent function' + ); + } + } + + public function testBug62291() { + $engine = $this->getEngine(); + $frame = $engine->getParser()->getPreprocessor()->newFrame(); + + $this->extraModules['Module:Bug62291'] = ' + local p = {} + function p.foo() + return table.concat( { + math.random(), math.random(), math.random(), math.random(), math.random() + }, ", " ) + end + function p.bar() + local t = {} + t[1] = p.foo() + t[2] = mw.getCurrentFrame():preprocess( "{{#invoke:Bug62291|bar2}}" ) + t[3] = p.foo() + return table.concat( t, "; " ) + end + function p.bar2() + return "bar2 called" + end + return p + '; + + $title = Title::makeTitle( NS_MODULE, 'Bug62291' ); + $module = $engine->fetchModuleFromParser( $title ); + + // Make sure multiple invokes return the same text + $r1 = $module->invoke( 'foo', $frame->newChild() ); + $r2 = $module->invoke( 'foo', $frame->newChild() ); + $this->assertSame( $r1, $r2, 'Multiple invokes returned different sets of random numbers' ); + + // Make sure a recursive invoke doesn't reset the PRNG + $r1 = $module->invoke( 'bar', $frame->newChild() ); + $r = explode( '; ', $r1 ); + $this->assertNotSame( $r[0], $r[2], 'Recursive invoke reset PRNG' ); + $this->assertSame( 'bar2 called', $r[1], 'Sanity check failed' ); + + // But a second invoke does + $r2 = $module->invoke( 'bar', $frame->newChild() ); + $this->assertSame( $r1, $r2, + 'Multiple invokes with recursive invoke returned different sets of random numbers' ); + } + + public function testOsDateTimeTTLs() { + $engine = $this->getEngine(); + $pp = $engine->getParser()->getPreprocessor(); + + $this->extraModules['Module:DateTime'] = ' + local p = {} + function p.day() + return os.date( "%d" ) + end + function p.AMPM() + return os.date( "%p" ) + end + function p.hour() + return os.date( "%H" ) + end + function p.minute() + return os.date( "%M" ) + end + function p.second() + return os.date( "%S" ) + end + function p.table() + return os.date( "*t" ) + end + function p.tablesec() + return os.date( "*t" ).sec + end + function p.time() + return os.time() + end + function p.specificDateAndTime() + return os.date("%S", os.time{year = 2013, month = 1, day = 1}) + end + return p + '; + + $title = Title::makeTitle( NS_MODULE, 'DateTime' ); + $module = $engine->fetchModuleFromParser( $title ); + + $frame = $pp->newFrame(); + $module->invoke( 'day', $frame ); + $this->assertNotNull( $frame->getTTL(), 'TTL must be set when day is requested' ); + $this->assertLessThanOrEqual( 86400, $frame->getTTL(), + 'TTL must not exceed 1 day when day is requested' ); + + $frame = $pp->newFrame(); + $module->invoke( 'AMPM', $frame ); + $this->assertNotNull( $frame->getTTL(), 'TTL must be set when AM/PM is requested' ); + $this->assertLessThanOrEqual( 43200, $frame->getTTL(), + 'TTL must not exceed 12 hours when AM/PM is requested' ); + + $frame = $pp->newFrame(); + $module->invoke( 'hour', $frame ); + $this->assertNotNull( $frame->getTTL(), 'TTL must be set when hour is requested' ); + $this->assertLessThanOrEqual( 3600, $frame->getTTL(), + 'TTL must not exceed 1 hour when hours are requested' ); + + $frame = $pp->newFrame(); + $module->invoke( 'minute', $frame ); + $this->assertNotNull( $frame->getTTL(), 'TTL must be set when minutes are requested' ); + $this->assertLessThanOrEqual( 60, $frame->getTTL(), + 'TTL must not exceed 1 minute when minutes are requested' ); + + $frame = $pp->newFrame(); + $module->invoke( 'second', $frame ); + $this->assertEquals( 1, $frame->getTTL(), + 'TTL must be equal to 1 second when seconds are requested' ); + + $frame = $pp->newFrame(); + $module->invoke( 'table', $frame ); + $this->assertNull( $frame->getTTL(), + 'TTL must not be set when os.date( "*t" ) is called but no values are looked at' ); + + $frame = $pp->newFrame(); + $module->invoke( 'tablesec', $frame ); + $this->assertEquals( 1, $frame->getTTL(), + 'TTL must be equal to 1 second when seconds are requested from a table' ); + + $frame = $pp->newFrame(); + $module->invoke( 'time', $frame ); + $this->assertEquals( 1, $frame->getTTL(), + 'TTL must be equal to 1 second when os.time() is called' ); + + $frame = $pp->newFrame(); + $module->invoke( 'specificDateAndTime', $frame ); + $this->assertNull( $frame->getTTL(), + 'TTL must not be set when os.date() or os.time() are called with a specific time' ); + } + + /** + * @dataProvider provideVolatileCaching + */ + public function testVolatileCaching( $func ) { + $engine = $this->getEngine(); + $parser = $engine->getParser(); + $pp = $parser->getPreprocessor(); + + $count = 0; + $parser->setHook( 'scribuntocount', function ( $str, $argv, $parser, $frame ) use ( &$count ) { + $frame->setVolatile(); + return ++$count; + } ); + + $this->extraModules['Template:ScribuntoTestVolatileCaching'] = '<scribuntocount/>'; + $this->extraModules['Module:TestVolatileCaching'] = ' + return { + preprocess = function ( frame ) + return frame:preprocess( "<scribuntocount/>" ) + end, + extensionTag = function ( frame ) + return frame:extensionTag( "scribuntocount" ) + end, + expandTemplate = function ( frame ) + return frame:expandTemplate{ title = "ScribuntoTestVolatileCaching" } + end, + } + '; + + $frame = $pp->newFrame(); + $count = 0; + $wikitext = "{{#invoke:TestVolatileCaching|$func}}"; + $text = $frame->expand( $pp->preprocessToObj( "$wikitext $wikitext" ) ); + $text = $parser->mStripState->unstripBoth( $text ); + $this->assertTrue( $frame->isVolatile(), "Frame is marked volatile" ); + $this->assertEquals( '1 2', $text, "Volatile wikitext was not cached" ); + } + + public function provideVolatileCaching() { + return [ + [ 'preprocess' ], + [ 'extensionTag' ], + [ 'expandTemplate' ], + ]; + } + + public function testGetCurrentFrameAndMWLoadData() { + $engine = $this->getEngine(); + $parser = $engine->getParser(); + $pp = $parser->getPreprocessor(); + + $this->extraModules['Module:Bug65687'] = ' + return { + test = function ( frame ) + return mw.loadData( "Module:Bug65687-LD" )[1] + end + } + '; + $this->extraModules['Module:Bug65687-LD'] = 'return { mw.getCurrentFrame().args[1] or "ok" }'; + + $frame = $pp->newFrame(); + $text = $frame->expand( $pp->preprocessToObj( "{{#invoke:Bug65687|test|foo}}" ) ); + $text = $parser->mStripState->unstripBoth( $text ); + $this->assertEquals( 'ok', $text, 'mw.loadData allowed access to frame args' ); + } + + public function testGetCurrentFrameAtModuleScope() { + $engine = $this->getEngine(); + $parser = $engine->getParser(); + $pp = $parser->getPreprocessor(); + + $this->extraModules['Module:Bug67498-directly'] = ' + local f = mw.getCurrentFrame() + local f2 = f and f.args[1] or "<none>" + + return { + test = function ( frame ) + return ( f and f.args[1] or "<none>" ) .. " " .. f2 + end + } + '; + $this->extraModules['Module:Bug67498-statically'] = ' + local M = require( "Module:Bug67498-directly" ) + return { + test = function ( frame ) + return M.test( frame ) + end + } + '; + $this->extraModules['Module:Bug67498-dynamically'] = ' + return { + test = function ( frame ) + local M = require( "Module:Bug67498-directly" ) + return M.test( frame ) + end + } + '; + + foreach ( [ 'directly', 'statically', 'dynamically' ] as $how ) { + $frame = $pp->newFrame(); + $text = $frame->expand( $pp->preprocessToObj( + "{{#invoke:Bug67498-$how|test|foo}} -- {{#invoke:Bug67498-$how|test|bar}}" + ) ); + $text = $parser->mStripState->unstripBoth( $text ); + $text = explode( ' -- ', $text ); + $this->assertEquals( 'foo foo', $text[0], + "mw.getCurrentFrame() failed from a module loaded $how" + ); + $this->assertEquals( 'bar bar', $text[1], + "mw.getCurrentFrame() cached the frame from a module loaded $how" + ); + } + } +} + +// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps +class Scribunto_LuaCommonTestsLibrary extends Scribunto_LuaLibraryBase { + public function register() { + $lib = [ + 'test' => [ $this, 'test' ], + ]; + $opts = [ + 'test' => 'Test option', + ]; + + return $this->getEngine()->registerInterface( __DIR__ . '/CommonTests-lib.lua', $lib, $opts ); + } + + public function test() { + return [ 'Test function' ]; + } +} + +// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps +class Scribunto_LuaCommonTestsFailLibrary extends Scribunto_LuaLibraryBase { + public function __construct() { + throw new MWException( 'deferLoad library that is never required was loaded anyway' ); + } + + public function register() { + } +} diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-data-fail1.lua b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-data-fail1.lua new file mode 100644 index 00000000..4ee62114 --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-data-fail1.lua @@ -0,0 +1,2 @@ +-- This is invalid for mw.loadData() +return "ok" diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-data-fail2.lua b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-data-fail2.lua new file mode 100644 index 00000000..36dab23f --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-data-fail2.lua @@ -0,0 +1,4 @@ +-- This is invalid for mw.loadData() +return { + function() end +} diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-data-fail3.lua b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-data-fail3.lua new file mode 100644 index 00000000..0b2bf0ca --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-data-fail3.lua @@ -0,0 +1,4 @@ +-- This is invalid for mw.loadData() +return { + setmetatable( {}, {} ) +} diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-data-fail4.lua b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-data-fail4.lua new file mode 100644 index 00000000..51d41142 --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-data-fail4.lua @@ -0,0 +1,4 @@ +-- This is invalid for mw.loadData() +return { + [function() end] = true +} diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-data-fail5.lua b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-data-fail5.lua new file mode 100644 index 00000000..0f17fb11 --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-data-fail5.lua @@ -0,0 +1,4 @@ +-- This is invalid for mw.loadData() +return { + [{}] = true +} diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-data.lua b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-data.lua new file mode 100644 index 00000000..11515fca --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-data.lua @@ -0,0 +1,20 @@ +-- This data is valid +local t = { + ["true"] = true, + ["false"] = false, + NaN = 0/0, + inf = 1/0, + num = 12.5, + str = "foo bar", + table = { + "one", "two", "three", foo = "bar" + } +} + +-- Duplicate values +t.table2 = t.table + +-- Make sure recursion is correctly handled, too +t.t = t + +return t diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-lib.lua b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-lib.lua new file mode 100644 index 00000000..8825ddb7 --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-lib.lua @@ -0,0 +1,30 @@ +local p = {} +local php +local options + +function p.setupInterface( opts ) + -- Boilerplate + p.setupInterface = nil + php = mw_interface + mw_interface = nil + options = opts + + -- Loaded dynamically, don't mess with globals like 'mw' or + -- 'package.loaded' +end + +function p.test() + return options.test, php.test() +end + +function p.setVal( frame ) + options.val = frame.args[1] +end + +function p.getVal( frame ) + return tostring( options.val ) +end + +p.foobar = { val = "nope" } + +return p diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests.lua b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests.lua new file mode 100644 index 00000000..fef18edf --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests.lua @@ -0,0 +1,387 @@ +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 + +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 + +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 = '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' }, + }, +} ) diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/HashLibraryTest.php b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/HashLibraryTest.php new file mode 100644 index 00000000..edb543c7 --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/HashLibraryTest.php @@ -0,0 +1,13 @@ +<?php + +// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps +class Scribunto_LuaHashLibraryTest extends Scribunto_LuaEngineTestBase { + protected static $moduleName = 'HashLibraryTests'; + + protected function getTestModules() { + return parent::getTestModules() + [ + 'HashLibraryTests' => __DIR__ . '/HashLibraryTests.lua', + ]; + } + +} diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/HashLibraryTests.lua b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/HashLibraryTests.lua new file mode 100644 index 00000000..f8186b72 --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/HashLibraryTests.lua @@ -0,0 +1,53 @@ +--[[ + Tests for the mw.hash module + + @license GNU GPL v2+ + @author Marius Hoch < hoo@online.de > +]] + +local testframework = require 'Module:TestFramework' + +local function testListAlgorithms() + local algos = mw.hash.listAlgorithms() + + if type( algos ) ~= 'table' then + return 'algo list was expected to be a table' + end + + for i, v in ipairs( algos ) do + if v == 'md5' then + return true + end + end + + return 'md5 was expected to be in the algo list' +end + +-- Tests +local tests = { + { name = 'mw.hash.listAlgorithms', func = testListAlgorithms, + expect = { true } + }, + { name = 'mw.hash.hashValue sha1', func = mw.hash.hashValue, + args = { 'sha1', 'abc' }, + expect = { 'a9993e364706816aba3e25717850c26c9cd0d89d' } + }, + { name = 'mw.hash.hashValue md5', func = mw.hash.hashValue, + args = { 'md5', 'abc' }, + expect = { '900150983cd24fb0d6963f7d28e17f72' } + }, + { name = 'mw.hash.hashValue bad argument type #1', func = mw.hash.hashValue, + args = { nil, 'a-string' }, + expect = "bad argument #1 to 'hashValue' (string expected, got nil)" + }, + { name = 'mw.hash.hashValue bad argument type #2', func = mw.hash.hashValue, + args = { 'abc', 2 }, + expect = "bad argument #2 to 'hashValue' (string expected, got number)" + }, + { name = 'mw.hash.hashValue bad algorithm', func = mw.hash.hashValue, + args = { 'not-a-hashing-algorithm', 'abc' }, + expect = "Unknown hashing algorithm: not-a-hashing-algorithm" + } +} + +return testframework.getTestProvider( tests ) diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/HtmlLibraryTest.php b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/HtmlLibraryTest.php new file mode 100644 index 00000000..98536c18 --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/HtmlLibraryTest.php @@ -0,0 +1,26 @@ +<?php + +// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps +class Scribunto_LuaHtmlLibraryTest extends Scribunto_LuaEngineTestBase { + protected static $moduleName = 'HtmlLibraryTests'; + + protected function setUp() { + parent::setUp(); + + // For strip marker test + $markers = [ + 'nowiki' => Parser::MARKER_PREFIX . '-test-nowiki-' . Parser::MARKER_SUFFIX, + ]; + $interpreter = $this->getEngine()->getInterpreter(); + $interpreter->callFunction( + $interpreter->loadString( 'mw.html.stripMarkers = ...', 'fortest' ), + $markers + ); + } + + protected function getTestModules() { + return parent::getTestModules() + [ + 'HtmlLibraryTests' => __DIR__ . '/HtmlLibraryTests.lua', + ]; + } +} diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/HtmlLibraryTests.lua b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/HtmlLibraryTests.lua new file mode 100644 index 00000000..e9ea1234 --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/HtmlLibraryTests.lua @@ -0,0 +1,359 @@ +--[[ + Tests for the mw.html module + + @license GNU GPL v2+ + @author Marius Hoch < hoo@online.de > +]] + +local testframework = require 'Module:TestFramework' + +local function getEmptyTestDiv() + return mw.html.create( 'div' ) +end + +local function testHelper( obj, method, ... ) + return obj[method]( obj, ... ) +end + +-- Test attrbutes which will always be paired in the same order +local testAttrs = { foo = 'bar', ab = 'cd' } +setmetatable( testAttrs, { __pairs = function ( t ) + local keys = { 'ab', 'foo' } + local i = 0 + return function() + i = i + 1 + if i <= #keys then + return keys[i], t[keys[i]] + end + end +end } ) + + +-- More complex test functions + +local function testMultiAddClass() + return getEmptyTestDiv():addClass( 'foo' ):addClass( 'bar' ) +end + +local function testCssAndCssText() + return getEmptyTestDiv():css( 'foo', 'bar' ):cssText( 'abc:def' ):css( 'g', 'h' ) +end + +local function testTagDone() + return getEmptyTestDiv():tag( 'span' ):done() +end + +local function testNodeDone() + return getEmptyTestDiv():node( getEmptyTestDiv() ):done() +end + +local function testTagNodeAllDone() + return getEmptyTestDiv():tag( 'p' ):node( getEmptyTestDiv() ):allDone() +end + +local function testAttributeOverride() + return getEmptyTestDiv():attr( 'good', 'MediaWiki' ):attr( 'good', 'Wikibase' ) +end + +local function testAttributeRemoval() + return getEmptyTestDiv():attr( 'a', 'b' ):attr( 'a', nil ) +end + +local function testGetAttribute() + return getEmptyTestDiv():attr( 'town', 'Berlin' ):getAttr( 'town' ) +end + +local function testGetAttributeEscaping() + return getEmptyTestDiv():attr( 'foo', '<ble"&rgh>' ):getAttr( 'foo' ) +end + +local function testNodeSelfClosingDone() + return getEmptyTestDiv():node( mw.html.create( 'br' ) ):done() +end + +local function testNodeAppendToSelfClosing() + return mw.html.create( 'img' ):node( getEmptyTestDiv() ) +end + +local function testWikitextAppendToSelfClosing() + return mw.html.create( 'hr' ):wikitext( 'foo' ) +end + +local function testCreateWithValue( val ) + return mw.html.create( val ):wikitext( 'foo' ):tag( 'div' ):attr( 'a', 'b' ):allDone() +end + +local function testCssRemoval() + return getEmptyTestDiv():css( 'color', 'red' ):css( 'color', nil ) +end + +local function testComplex() + local builder = getEmptyTestDiv() + + builder:addClass( 'firstClass' ):attr( 'what', 'ever' ) + + builder:tag( 'meh' ):attr( 'whynot', 'Русский' ):tag( 'hr' ):attr( 'a', 'b' ) + + builder:node( mw.html.create( 'hr' ) ) + + builder:node( getEmptyTestDiv():attr( 'abc', 'def' ):css( 'width', '-1px' ) ) + + return builder +end + +local function testStripMarker() + local expect = '<div foo="' .. mw.html.stripMarkers.nowiki .. '"></div>' + local actual = tostring( getEmptyTestDiv():attr( 'foo', mw.html.stripMarkers.nowiki ) ) + if actual ~= expect then + error( actual .. ' ~= ' .. expect ) + end + return 'ok' +end + +-- Tests +local tests = { + -- Simple (inline) tests + { name = 'mw.html.create', func = mw.html.create, type='ToString', + args = { 'table' }, + expect = { '<table></table>' } + }, + { name = 'mw.html.create (self closing)', func = mw.html.create, type='ToString', + args = { 'br' }, + expect = { '<br />' } + }, + { name = 'mw.html.create (self closing - forced)', func = mw.html.create, type='ToString', + args = { 'div', { selfClosing = true } }, + expect = { '<div />' } + }, + { name = 'mw.html.create (invalid tag 1)', func = mw.html.create, type='ToString', + args = { '$$$$' }, + expect = "invalid tag name '$$$$'" + }, + { name = 'mw.html.create (invalid tag 2)', func = mw.html.create, type='ToString', + args = { {} }, + expect = "bad argument #1 to 'mw.html.create' (string expected, got table)" + }, + { name = 'mw.html.wikitext', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'wikitext', 'Plain text' }, + expect = { '<div>Plain text</div>' } + }, + { name = 'mw.html.wikitext (invalid input)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'wikitext', 'Plain text', {} }, + expect = "bad argument #2 to 'wikitext' (string or number expected, got table)" + }, + { name = 'mw.html.newline', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'newline' }, + expect = { '<div>\n</div>' } + }, + { name = 'mw.html.tag', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'tag', 'span' }, + -- tag is only supposed to return the new (inner) node + expect = { '<span></span>' } + }, + { name = 'mw.html.attr', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'attr', 'foo', 'bar' }, + expect = { '<div foo="bar"></div>' } + }, + { name = 'mw.html.attr (nil noop)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'attr', 'foo', nil }, + expect = { '<div></div>' } + }, + { name = 'mw.html.attr (table 1)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'attr', { foo = 'bar' } }, + expect = { '<div foo="bar"></div>' } + }, + { name = 'mw.html.attr (table 2)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'attr', testAttrs }, + expect = { '<div ab="cd" foo="bar"></div>' } + }, + { name = 'mw.html.attr (invalid name 1)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'attr', 123, 'bar' }, + expect = "bad argument #1 to 'attr' (string expected, got number)" + }, + { name = 'mw.html.attr (invalid name 2)', func = testHelper, + args = { getEmptyTestDiv(), 'attr', '§§§§', 'foo' }, + expect = "bad argument #1 to 'attr' (invalid attribute name '§§§§')" + }, + { name = 'mw.html.attr (table no value)', func = testHelper, + args = { getEmptyTestDiv(), 'attr', { foo = 'bar' }, 'foo' }, + expect = "bad argument #2 to 'attr' (if argument #1 is a table, argument #2 must be left empty)" + }, + { name = 'mw.html.attr (invalid value)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'attr', 'foo', true }, + expect = "bad argument #2 to 'attr' (string, number or nil expected, got boolean)" + }, + { name = 'mw.html.attr (invalid table 1)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'attr', { foo = {} } }, + expect = "bad argument #1 to 'attr' " .. + '(table keys must be strings, and values must be strings or numbers)' + }, + { name = 'mw.html.attr (invalid table 2)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'attr', { 1, 2 ,3 } }, + expect = "bad argument #1 to 'attr' " .. + '(table keys must be strings, and values must be strings or numbers)' + }, + { name = 'mw.html.attr (invalid table 3)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'attr', { foo = 'bar', blah = true } }, + expect = "bad argument #1 to 'attr' " .. + '(table keys must be strings, and values must be strings or numbers)' + }, + { name = 'mw.html.attr (invalid table 4)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'attr', { [{}] = 'foo' } }, + expect = "bad argument #1 to 'attr' " .. + '(table keys must be strings, and values must be strings or numbers)' + }, + { name = 'mw.html.getAttr (nil)', func = testHelper, + args = { getEmptyTestDiv(), 'getAttr', 'foo' }, + expect = { nil } + }, + { name = 'mw.html.getAttr (invalid name)', func = testHelper, + args = { getEmptyTestDiv(), 'getAttr', 123 }, + expect = "bad argument #1 to 'getAttr' (string expected, got number)" + }, + { name = 'mw.html.addClass', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'addClass', 'foo' }, + expect = { '<div class="foo"></div>' } + }, + { name = 'mw.html.addClass (numeric argument)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'addClass', 123 }, + expect = { '<div class="123"></div>' } + }, + { name = 'mw.html.addClass (invalid value)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'addClass', {} }, + expect = "bad argument #1 to 'addClass' (string, number or nil expected, got table)" + }, + { name = 'mw.html.css', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'css', 'foo', 'bar' }, + expect = { '<div style="foo:bar"></div>' } + }, + { name = 'mw.html.css (numeric arguments)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'css', 123, 456 }, + expect = { '<div style="123:456"></div>' } + }, + { name = 'mw.html.css (nil noop)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'css', 'foo', nil }, + expect = { '<div></div>' } + }, + { name = 'mw.html.css (invalid name 1)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'css', function() end, 'bar' }, + expect = "bad argument #1 to 'css' (string or number expected, got function)" + }, + { name = 'mw.html.css (table no value)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'css', {}, 'bar' }, + expect = "bad argument #2 to 'css' (if argument #1 is a table, argument #2 must be left empty)" + }, + { name = 'mw.html.css (invalid value)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'css', 'foo', {} }, + expect = "bad argument #2 to 'css' (string, number or nil expected, got table)" + }, + { name = 'mw.html.css (table)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'css', testAttrs }, + expect = { '<div style="ab:cd;foo:bar"></div>' } + }, + { name = 'mw.html.css (invalid table)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'css', { foo = 'bar', ab = true } }, + expect = "bad argument #1 to 'css' " .. + '(table keys and values must be strings or numbers)' + }, + { name = 'mw.html.cssText', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'cssText', 'Unit tests, ftw' }, + expect = { '<div style="Unit tests, ftw"></div>' } + }, + { name = 'mw.html.cssText (numeric argument)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'cssText', 123 }, + expect = { '<div style="123"></div>' } + }, + { name = 'mw.html.cssText (invalid value)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'cssText', {} }, + expect = "bad argument #1 to 'cssText' (string, number or nil expected, got table)" + }, + { name = 'mw.html attribute escaping (value with double quotes)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'attr', 'foo', 'ble"rgh' }, + expect = { '<div foo="ble"rgh"></div>' } + }, + { name = 'mw.html attribute escaping 1', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'attr', 'foo', 'ble<rgh' }, + expect = { '<div foo="ble<rgh"></div>' } + }, + { name = 'mw.html attribute escaping 2', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'attr', 'foo', '<ble"&rgh>' }, + expect = { '<div foo="<ble"&rgh>"></div>' } + }, + { name = 'mw.html attribute escaping (CSS)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'css', 'mu"ha', 'ha"ha' }, + expect = { '<div style="mu"ha:ha"ha"></div>' } + }, + { name = 'mw.html attribute escaping (CSS raw)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'cssText', 'mu"ha:-ha"ha' }, + expect = { '<div style="mu"ha:-ha"ha"></div>' } + }, + { name = 'mw.html.addClass (nil)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'addClass' }, + expect = { '<div></div>' } + }, + { name = 'mw.html.cssText (nil)', func = testHelper, type='ToString', + args = { getEmptyTestDiv(), 'cssText' }, + expect = { '<div></div>' } + }, + + -- Tests defined above + + { name = 'mw.html.addClass (twice) ', func = testMultiAddClass, type='ToString', + expect = { '<div class="foo bar"></div>' } + }, + { name = 'mw.html.css.cssText.css', func = testCssAndCssText, type='ToString', + expect = { '<div style="foo:bar;abc:def;g:h"></div>' } + }, + { name = 'mw.html.tag (using done)', func = testTagDone, type='ToString', + expect = { '<div><span></span></div>' } + }, + { name = 'mw.html.node (using done)', func = testNodeDone, type='ToString', + expect = { '<div><div></div></div>' } + }, + { name = 'mw.html.node (self closing, using done)', func = testNodeSelfClosingDone, type='ToString', + expect = { '<div><br /></div>' } + }, + { name = 'mw.html.node (append to self closing)', func = testNodeAppendToSelfClosing, type='ToString', + expect = "self-closing tags can't have child nodes" + }, + { name = 'mw.html.wikitext (append to self closing)', func = testWikitextAppendToSelfClosing, type='ToString', + expect = "self-closing tags can't have child nodes" + }, + { name = 'mw.html.tag.node (using allDone)', func = testTagNodeAllDone, type='ToString', + expect = { '<div><p><div></div></p></div>' } + }, + { name = 'mw.html.attr (overrides)', func = testAttributeOverride, type='ToString', + expect = { '<div good="Wikibase"></div>' } + }, + { name = 'mw.html.attr (removal)', func = testAttributeRemoval, type='ToString', + expect = { '<div></div>' } + }, + { name = 'mw.html.getAttr', func = testGetAttribute, type='ToString', + expect = { 'Berlin' } + }, + { name = 'mw.html.getAttr (escaping)', func = testGetAttributeEscaping, type='ToString', + expect = { '<ble"&rgh>' } + }, + { name = 'mw.html.create (empty string)', func = testCreateWithValue, type='ToString', + args = {''}, + expect = { 'foo<div a="b"></div>' } + }, + { name = 'mw.html.create (nil)', func = testCreateWithValue, type='ToString', + args = {nil}, + expect = { 'foo<div a="b"></div>' } + }, + { name = 'mw.html.css (removal)', func = testCssRemoval, type='ToString', + expect = { '<div></div>' } + }, + { name = 'mw.html complex test', func = testComplex, type='ToString', + expect = { + '<div class="firstClass" what="ever"><meh whynot="Русский"><hr a="b" /></meh>' .. + '<hr /><div abc="def" style="width:-1px"></div></div>' + } + }, + { name = 'mw.html strip marker test', func = testStripMarker, type='ToString', + expect = { 'ok' } + }, +} + +return testframework.getTestProvider( tests ) diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LanguageLibraryTest.php b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LanguageLibraryTest.php new file mode 100644 index 00000000..39a631b2 --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LanguageLibraryTest.php @@ -0,0 +1,70 @@ +<?php + +// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps +class Scribunto_LuaLanguageLibraryTest extends Scribunto_LuaEngineTestBase { + protected static $moduleName = 'LanguageLibraryTests'; + + public function __construct( + $name = null, array $data = [], $dataName = '', $engineName = null + ) { + parent::__construct( $name, $data, $dataName, $engineName ); + + // Skip certain tests if something isn't providing translated language names + // (bug 67343) + if ( Language::fetchLanguageName( 'en', 'fr' ) === 'English' ) { + $msg = 'Language name translations are unavailable; ' . + 'install Extension:CLDR or something similar'; + $this->skipTests += [ + 'fetchLanguageName (en,ru)' => $msg, + 'fetchLanguageName (ru,en)' => $msg, + 'fetchLanguageNames (de)' => $msg, + 'fetchLanguageNames ([[bogus]])' => $msg, + ]; + } + } + + protected function getTestModules() { + return parent::getTestModules() + [ + 'LanguageLibraryTests' => __DIR__ . '/LanguageLibraryTests.lua', + ]; + } + + public function testFormatDateTTLs() { + global $wgContLang; + + $engine = $this->getEngine(); + $pp = $engine->getParser()->getPreprocessor(); + + $ttl = null; + $wgContLang->sprintfDate( 's', '20130101000000', null, $ttl ); + if ( $ttl === null ) { + $this->markTestSkipped( "Language::sprintfDate does not set a TTL" ); + } + + // sprintfDate has its own unit tests for making sure its output is right, + // so all we need to test here is we get TTLs when we're supposed to + $this->extraModules['Module:FormatDate'] = ' + local p = {} + function p.formatCurrentDate() + return mw.getContentLanguage():formatDate( "s" ) + end + function p.formatSpecificDate() + return mw.getContentLanguage():formatDate( "s", "20130101000000" ) + end + return p + '; + + $title = Title::makeTitle( NS_MODULE, 'FormatDate' ); + $module = $engine->fetchModuleFromParser( $title ); + + $frame = $pp->newFrame(); + $module->invoke( 'formatCurrentDate', $frame ); + $this->assertEquals( 1, $frame->getTTL(), + 'TTL must be equal to 1 second when lang:formatDate( \'s\' ) is called' ); + + $frame = $pp->newFrame(); + $module->invoke( 'formatSpecificDate', $frame ); + $this->assertNull( $frame->getTTL(), + 'TTL must not be set when lang:formatDate is called with a specific date' ); + } +} diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LanguageLibraryTests.lua b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LanguageLibraryTests.lua new file mode 100644 index 00000000..2fc2ab6f --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LanguageLibraryTests.lua @@ -0,0 +1,435 @@ +local testframework = require 'Module:TestFramework' + +local langs = nil +local function getLangs() + if langs == nil then + langs = { + mw.language.new( 'en' ), + mw.language.new( 'kaa' ), + mw.language.new( 'fa' ), + mw.language.new( '[[bogus]]' ), + } + end + return langs +end + +local function test_method( func, ... ) + local langs = getLangs() + + local ret = {} + for i = 1, #langs do + local got = { pcall( langs[i][func], langs[i], ... ) } + if table.remove( got, 1 ) then + ret[i] = got + else + ret[i] = string.gsub( got[1], '^%S+:%d+: ', '' ) + end + end + return unpack( ret ) +end + +local function test_method_lang( lang, func, ... ) + local obj = mw.language.new( lang ) + return obj[func]( obj, ... ) +end + +local function test_plural( lang ) + local obj = mw.language.new( lang ) + local ret1, ret2 = '', '' + local ret3, ret4 = '', '' + for i = 0, 29 do + ret1 = ret1 .. obj:convertPlural( i, 'a', 'b', 'c', 'd', 'e' ) + ret2 = ret2 .. obj:convertPlural( i, { 'a', 'b', 'c', 'd', 'e' } ) + ret3 = ret3 .. obj:plural( i, 'a', 'b', 'c', 'd', 'e' ) + ret4 = ret4 .. obj:plural( i, { 'a', 'b', 'c', 'd', 'e' } ) + end + if ret1 ~= ret2 or ret1 ~= ret3 or ret1 ~= ret4 then + error( "Plural tests don't match:" .. + " " .. ret1 + " " .. ret2 + " " .. ret3 + " " .. ret4 + ) + end + return ret1 +end + +local function test_multi( func, ... ) + local ret = {} + for i = 1, select( '#', ... ) do + ret[i] = func( select( i, ... ) ) + end + return unpack( ret, 1, select( '#', ... ) ) +end + +local function test_fetchLanguageNames( ... ) + local ret = mw.language.fetchLanguageNames( ... ) + if type( ret ) == 'table' then + return { + en = ret.en, + ru = ret.ru, + } + else + return ret + end +end + +local function test_parseFormattedNumber() + local langs = getLangs() + + local ret = {} + for i = 1, #langs do + local ok, num = pcall( langs[i].formatNum, langs[i], 123456.78901 ) + local got = { pcall( langs[i].parseFormattedNumber, langs[i], num ) } + if table.remove( got, 1 ) then + ret[i] = got + else + ret[i] = string.gsub( got[1], '^%S+:%d+: ', '' ) + end + end + return unpack( ret ) +end + +return testframework.getTestProvider( { + { name = 'fetchLanguageName (en)', func = mw.language.fetchLanguageName, + args = { 'en' }, + expect = { 'English' } + }, + { name = 'fetchLanguageName (ru)', func = mw.language.fetchLanguageName, + args = { 'ru' }, + expect = { 'русский' } + }, + { name = 'fetchLanguageName (en,ru)', func = mw.language.fetchLanguageName, + args = { 'en', 'ru' }, + expect = { 'английский' } + }, + { name = 'fetchLanguageName (ru,en)', func = mw.language.fetchLanguageName, + args = { 'ru', 'en' }, + expect = { 'Russian' } + }, + { name = 'fetchLanguageName ([[bogus]])', func = mw.language.fetchLanguageName, + args = { '[[bogus]]' }, + expect = { '' } + }, + { name = 'fetchLanguageName (en,[[bogus]])', func = mw.language.fetchLanguageName, + args = { 'en', '[[bogus]]' }, + expect = { 'English' } + }, + + { name = 'fetchLanguageNames ()', func = test_fetchLanguageNames, + args = {}, + expect = { { en = 'English', ru = 'русский' } } + }, + { name = 'fetchLanguageNames (de)', func = test_fetchLanguageNames, + args = { 'de' }, + expect = { { en = 'Englisch', ru = 'Russisch' } } + }, + { name = 'fetchLanguageNames ([[bogus]])', func = test_fetchLanguageNames, + args = { '[[bogus]]' }, + expect = { { en = 'English', ru = 'Russian' } } + }, + + { name = 'getFallbacksFor', func = test_multi, + args = { mw.language.getFallbacksFor, 'en', 'de', 'arz', '[[bogus]]' }, + expect = { {}, { 'en' }, { 'ar', 'en' }, {} } + }, + + { name = 'isKnownLanguageTag', func = test_multi, + args = { mw.language.isKnownLanguageTag, 'en', 'not-a-real-code', 'extension code', '[[bogus]]' }, + expect = { true, false, false, false } + }, + + { name = 'isSupportedLanguage', func = test_multi, + args = { mw.language.isSupportedLanguage, 'en', 'not-a-real-code', 'extension code', '[[bogus]]' }, + expect = { true, false, false, false } + }, + + { name = 'isValidBuiltInCode', func = test_multi, + args = { mw.language.isValidBuiltInCode, 'en', 'not-a-real-code', 'extension code', '[[bogus]]' }, + expect = { true, true, false, false } + }, + + { name = 'isValidCode', func = test_multi, + args = { mw.language.isValidCode, 'en', 'not-a-real-code', 'extension code', '[[bogus]]' }, + expect = { true, true, true, false } + }, + + { name = 'mw.language.new', func = test_multi, type = 'ToString', + args = { mw.language.new, 'en', 'ru', '[[bogus]]' }, + expect = { 'table', 'table', 'table' } + }, + + { name = 'lang:getCode', func = test_method, + args = { 'getCode' }, + expect = { + { 'en' }, + { 'kaa' }, + { 'fa' }, + { '[[bogus]]' }, + } + }, + + { name = 'lang:getFallbackLanguages', func = test_method, + args = { 'getFallbackLanguages' }, + expect = { + { {} }, + { { 'kk-latn', 'kk-cyrl', 'en' } }, + { { 'en' } }, + { {} }, + } + }, + + { name = 'lang:isRTL', func = test_method, + args = { 'isRTL' }, + expect = { + { false }, + { false }, + { true }, + "language code '[[bogus]]' is invalid", + } + }, + + { name = 'lang:lc', func = test_method, + args = { 'lc', 'IX' }, + expect = { + { 'ix' }, + { 'ix' }, -- Probably not actually right, but it's what LanguageKaa returns + { 'ix' }, + "language code '[[bogus]]' is invalid", + } + }, + + { name = 'lang:lcfirst', func = test_method, + args = { 'lcfirst', 'IX' }, + expect = { + { 'iX' }, + { 'ıX' }, + { 'iX' }, + "language code '[[bogus]]' is invalid", + } + }, + + { name = 'lang:uc', func = test_method, + args = { 'uc', 'ix' }, + expect = { + { 'IX' }, + { 'IX' }, -- Probably not actually right, but it's what LanguageKaa returns + { 'IX' }, + "language code '[[bogus]]' is invalid", + } + }, + + { name = 'lang:ucfirst', func = test_method, + args = { 'ucfirst', 'ix' }, + expect = { + { 'Ix' }, + { 'İx' }, + { 'Ix' }, + "language code '[[bogus]]' is invalid", + } + }, + + { name = 'lang:caseFold', func = test_method, + args = { 'caseFold', 'ix' }, + expect = { + { 'IX' }, + { 'IX' }, -- Probably not actually right, but it's what LanguageKaa returns + { 'IX' }, + "language code '[[bogus]]' is invalid", + } + }, + + { name = 'lang:formatNum', func = test_method, + args = { 'formatNum', 123456.78901 }, + expect = { + { '123,456.78901' }, + { "123\194\160456,78901" }, + { '۱۲۳٬۴۵۶٫۷۸۹۰۱' }, + "language code '[[bogus]]' is invalid", + } + }, + + { name = 'lang:formatDate', func = test_method, + args = { 'formatDate', 'Y-F-d H:i:s', '20140305123456' }, + expect = { + { '2014-March-05 12:34:56' }, + { '2014-Mart-05 12:34:56' }, + { '۲۰۱۴-مارس-۰۵ ۱۲:۳۴:۵۶' }, + "language code '[[bogus]]' is invalid", + } + }, + + { name = 'lang:formatDuration', func = test_method, + args = { 'formatDuration', 86461 }, + expect = { + { "1 day, 1 minute and 1 second" }, + { "1 күн, 1 минут ha'm 1 секунд" }, + { "۱ روز، ۱ دقیقه و ۱ ثانیه" }, + "language code '[[bogus]]' is invalid", + } + }, + { name = 'lang:formatDuration (hours and minutes)', func = test_method, + args = { 'formatDuration', 86461, { 'hours', 'minutes' } }, + expect = { + { "24 hours and 1 minute" }, + { "24 сағат ha'm 1 минут" }, + { "۲۴ ساعت و ۱ دقیقه" }, + "language code '[[bogus]]' is invalid", + } + }, + + { name = 'lang:parseFormattedNumber', func = test_parseFormattedNumber, + args = {}, + expect = { + { 123456.78901 }, + { 123456.78901 }, + { 123456.78901 }, + "language code '[[bogus]]' is invalid", + } + }, + + { name = 'lang:convertPlural (en)', func = test_plural, + args = { 'en' }, + expect = { 'babbbbbbbbbbbbbbbbbbbbbbbbbbbb' } + }, + { name = 'lang:convertPlural (pl)', func = test_plural, + args = { 'pl' }, + expect = { 'cabbbcccccccccccccccccbbbccccc' } + }, + { name = 'lang:convertPlural (bogus)', func = test_plural, + args = { '[[bogus]]' }, + expect = "language code '[[bogus]]' is invalid", + }, + + { name = 'lang:convertGrammar (ru)', func = test_method_lang, + args = { 'ru', 'convertGrammar', '**ия', 'genitive' }, + expect = { '**ии' } + }, + { name = 'lang:convertGrammar (bogus)', func = test_method_lang, + args = { '[[bogus]]', 'convertGrammar', '**ия', 'genitive' }, + expect = "language code '[[bogus]]' is invalid", + }, + + { name = 'lang:grammar (ru)', func = test_method_lang, + args = { 'ru', 'grammar', 'genitive', '**ия' }, + expect = { '**ии' } + }, + { name = 'lang:grammar (bogus)', func = test_method_lang, + args = { '[[bogus]]', 'grammar', 'genitive', '**ия' }, + expect = "language code '[[bogus]]' is invalid", + }, + + { name = 'lang:gender (male)', func = test_method, + args = { 'gender', 'male', 'masculine', 'feminine', 'neutral' }, + expect = { + { 'masculine' }, + { 'masculine' }, + { 'masculine' }, + "language code '[[bogus]]' is invalid", + } + }, + { name = 'lang:gender (female)', func = test_method, + args = { 'gender', 'female', 'masculine', 'feminine', 'neutral' }, + expect = { + { 'feminine' }, + { 'feminine' }, + { 'feminine' }, + "language code '[[bogus]]' is invalid", + } + }, + { name = 'lang:gender (male, with sequence)', func = test_method, + args = { 'gender', 'male', { 'masculine', 'feminine', 'neutral' } }, + expect = { + { 'masculine' }, + { 'masculine' }, + { 'masculine' }, + "language code '[[bogus]]' is invalid", + } + }, + + { name = 'lang:getArrow (forward)', func = test_method, + args = { 'getArrow', 'forwards' }, + expect = { + { "→" }, + { "→" }, + { "←" }, + "language code '[[bogus]]' is invalid", + } + }, + { name = 'lang:getArrow (right)', func = test_method, + args = { 'getArrow', 'right' }, + expect = { + { "→" }, + { "→" }, + { "→" }, + { "→" }, + } + }, + + { name = 'lang:getDir', func = test_method, + args = { 'getDir' }, + expect = { + { "ltr" }, + { "ltr" }, + { "rtl" }, + "language code '[[bogus]]' is invalid", + } + }, + + { name = 'lang:getDirMark', func = test_method, + args = { 'getDirMark' }, + expect = { + { "\226\128\142" }, + { "\226\128\142" }, + { "\226\128\143" }, + "language code '[[bogus]]' is invalid", + } + }, + { name = 'lang:getDirMark opposite', func = test_method, + args = { 'getDirMark', true }, + expect = { + { "\226\128\143" }, + { "\226\128\143" }, + { "\226\128\142" }, + "language code '[[bogus]]' is invalid", + } + }, + + { name = 'lang:getDirMarkEntity', func = test_method, + args = { 'getDirMarkEntity' }, + expect = { + { "‎" }, + { "‎" }, + { "‏" }, + "language code '[[bogus]]' is invalid", + } + }, + { name = 'lang:getDirMarkEntity opposite', func = test_method, + args = { 'getDirMarkEntity', true }, + expect = { + { "‏" }, + { "‏" }, + { "‎" }, + "language code '[[bogus]]' is invalid", + } + }, + + { name = 'lang:getDurationIntervals', func = test_method, + args = { 'getDurationIntervals', 86461 }, + expect = { + { { days = 1, minutes = 1, seconds = 1 } }, + { { days = 1, minutes = 1, seconds = 1 } }, + { { days = 1, minutes = 1, seconds = 1 } }, + "language code '[[bogus]]' is invalid", + } + }, + { name = 'lang:getDurationIntervals (hours and minutes)', func = test_method, + args = { 'getDurationIntervals', 86461, { 'hours', 'minutes' } }, + expect = { + { { hours = 24, minutes = 1 } }, + { { hours = 24, minutes = 1 } }, + { { hours = 24, minutes = 1 } }, + "language code '[[bogus]]' is invalid", + } + }, +} ) diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LibraryUtilTest.php b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LibraryUtilTest.php new file mode 100644 index 00000000..cf887392 --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LibraryUtilTest.php @@ -0,0 +1,12 @@ +<?php + +// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps +class Scribunto_LuaLibraryUtilTest extends Scribunto_LuaEngineTestBase { + protected static $moduleName = 'LibraryUtilTests'; + + function getTestModules() { + return parent::getTestModules() + [ + 'LibraryUtilTests' => __DIR__ . '/LibraryUtilTests.lua', + ]; + } +} diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LibraryUtilTests.lua b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LibraryUtilTests.lua new file mode 100644 index 00000000..8855742c --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LibraryUtilTests.lua @@ -0,0 +1,220 @@ +--[[ + Tests for the libraryUtil module + + @license GNU GPL v2+ + @author Mr. Stradivarius < misterstrad@gmail.com > +]] + +local testframework = require 'Module:TestFramework' + +local util = require( 'libraryUtil' ) +local checkType = util.checkType +local checkTypeMulti = util.checkTypeMulti +local checkTypeForIndex = util.checkTypeForIndex +local checkTypeForNamedArg = util.checkTypeForNamedArg +local makeCheckSelfFunction = util.makeCheckSelfFunction + +local function testExpectTypes( arg, expectTypes ) + pcall( checkTypeMulti, 'myFunc', 1, arg, expectTypes ) + return unpack( expectTypes ) +end + +local function testCheckSelf( self, method, ... ) + local checkSelf = makeCheckSelfFunction( ... ) + return checkSelf( self, method ) +end + +local testObject = {} + +-- Tests +local tests = { + -- checkType + { name = 'checkType, valid', func = checkType, type='ToString', + args = { 'myFunc', 1, 'foo', 'string' }, + expect = { nil } + }, + { name = 'checkType, invalid', func = checkType, type='ToString', + args = { 'myFunc', 1, 9, 'string' }, + expect = "bad argument #1 to 'myFunc' (string expected, got number)" + }, + { name = 'checkType, nil valid', func = checkType, type='ToString', + args = { 'myFunc', 1, nil, 'string', true }, + expect = { nil } + }, + { name = 'checkType, nil invalid', func = checkType, type='ToString', + args = { 'myFunc', 1, nil, 'string', false }, + expect = "bad argument #1 to 'myFunc' (string expected, got nil)" + }, + { name = 'checkType, boolean', func = checkType, type='ToString', + args = { 'myFunc', 1, true, 'boolean' }, + expect = { nil } + }, + { name = 'checkType, table', func = checkType, type='ToString', + args = { 'myFunc', 1, {}, 'table' }, + expect = { nil } + }, + { name = 'checkType, function', func = checkType, type='ToString', + args = { 'myFunc', 1, function () return end, 'function' }, + expect = { nil } + }, + { name = 'checkType, argument #2', func = checkType, type='ToString', + args = { 'myFunc', 2, 9, 'string' }, + expect = "bad argument #2 to 'myFunc' (string expected, got number)" + }, + { name = 'checkType, name', func = checkType, type='ToString', + args = { 'otherFunc', 1, 9, 'string' }, + expect = "bad argument #1 to 'otherFunc' (string expected, got number)" + }, + + -- checkTypeMulti + { name = 'checkTypeMulti, single valid', func = checkTypeMulti, type='ToString', + args = { 'myFunc', 1, 'foo', { 'string' } }, + expect = { nil } + }, + { name = 'checkTypeMulti, single type invalid (1)', func = checkTypeMulti, type='ToString', + args = { 'myFunc', 1, 9, { 'string' } }, + expect = "bad argument #1 to 'myFunc' (string expected, got number)" + }, + { name = 'checkTypeMulti, single type invalid (2)', func = checkTypeMulti, type='ToString', + args = { 'myFunc', 1, nil, { 'string' } }, + expect = "bad argument #1 to 'myFunc' (string expected, got nil)" + }, + { name = 'checkTypeMulti, multiple types valid (1)', func = checkTypeMulti, type='ToString', + args = { 'myFunc', 1, 'foo', { 'string', 'number', 'table' } }, + expect = { nil } + }, + { name = 'checkTypeMulti, multiple types valid (2)', func = checkTypeMulti, type='ToString', + args = { 'myFunc', 1, 9, { 'string', 'number', 'table' } }, + expect = { nil } + }, + { name = 'checkTypeMulti, multiple types valid (3)', func = checkTypeMulti, type='ToString', + args = { 'myFunc', 1, {}, { 'string', 'number', 'table' } }, + expect = { nil } + }, + { name = 'checkTypeMulti, multiple types invalid (1)', func = checkTypeMulti, type='ToString', + args = { 'myFunc', 1, true, { 'string', 'number', 'table' } }, + expect = "bad argument #1 to 'myFunc' (string, number or table expected, got boolean)" + }, + { name = 'checkTypeMulti, multiple types invalid (2)', func = checkTypeMulti, type='ToString', + args = { 'myFunc', 1, nil, { 'string', 'number', 'table' } }, + expect = "bad argument #1 to 'myFunc' (string, number or table expected, got nil)" + }, + { name = 'checkTypeMulti, multiple types invalid (3)', func = checkTypeMulti, type='ToString', + args = { 'myFunc', 1, function () return end, { 'string', 'number', 'table' } }, + expect = "bad argument #1 to 'myFunc' (string, number or table expected, got function)" + }, + { name = 'checkTypeMulti, two types invalid', func = checkTypeMulti, type='ToString', + args = { 'myFunc', 1, {}, { 'string', 'number' } }, + expect = "bad argument #1 to 'myFunc' (string or number expected, got table)" + }, + { name = 'checkTypeMulti, type order', func = checkTypeMulti, type='ToString', + args = { 'myFunc', 1, true, { 'table', 'number', 'string' } }, + expect = "bad argument #1 to 'myFunc' (table, number or string expected, got boolean)" + }, + { name = 'checkTypeMulti, argument #2', func = checkTypeMulti, type='ToString', + args = { 'myFunc', 2, 9, { 'string' } }, + expect = "bad argument #2 to 'myFunc' (string expected, got number)" + }, + { name = 'checkTypeMulti, other name', func = checkTypeMulti, type='ToString', + args = { 'otherFunc', 1, 9, { 'string' } }, + expect = "bad argument #1 to 'otherFunc' (string expected, got number)" + }, + { name = 'checkTypeMulti, expectTypes not altered (1)', func = testExpectTypes, type='ToString', + args = { 'foo', { 'string', 'number', 'table' } }, + expect = { 'string', 'number', 'table' } + }, + { name = 'checkTypeMulti, expectTypes not altered (2)', func = testExpectTypes, type='ToString', + args = { true, { 'string', 'number', 'table' } }, + expect = { 'string', 'number', 'table' } + }, + { name = 'checkTypeMulti, expectTypes not altered (3)', func = testExpectTypes, type='ToString', + args = { 'foo', { 'string' } }, + expect = { 'string' } + }, + { name = 'checkTypeMulti, expectTypes not altered (4)', func = testExpectTypes, type='ToString', + args = { true, { 'string' } }, + expect = { 'string' } + }, + + -- checkTypeForIndex + { name = 'checkTypeForIndex, valid', func = checkTypeForIndex, type='ToString', + args = { 'foo', 'bar', 'string' }, + expect = { nil } + }, + { name = 'checkTypeForIndex, invalid (1)', func = checkTypeForIndex, type='ToString', + args = { 'foo', 9, 'string' }, + expect = "value for index 'foo' must be string, number given" + }, + { name = 'checkTypeForIndex, invalid (2)', func = checkTypeForIndex, type='ToString', + args = { 'foo', 9, 'string' }, + expect = "value for index 'foo' must be string, number given" + }, + { name = 'checkTypeForIndex, other index', func = checkTypeForIndex, type='ToString', + args = { 'bar', 9, 'string' }, + expect = "value for index 'bar' must be string, number given" + }, + + -- checkTypeForNamedArg + { name = 'checkTypeForNamedArg, valid', func = checkTypeForNamedArg, type='ToString', + args = { 'myFunc', 'myArg', 'foo', 'string' }, + expect = { nil } + }, + { name = 'checkTypeForNamedArg, invalid', func = checkTypeForNamedArg, type='ToString', + args = { 'myFunc', 'myArg', 9, 'string' }, + expect = "bad named argument myArg to 'myFunc' (string expected, got number)" + }, + { name = 'checkTypeForNamedArg, nil valid', func = checkTypeForNamedArg, type='ToString', + args = { 'myFunc', 'myArg', nil, 'string', true }, + expect = { nil } + }, + { name = 'checkTypeForNamedArg, nil invalid', func = checkTypeForNamedArg, type='ToString', + args = { 'myFunc', 'myArg', nil, 'string', false }, + expect = "bad named argument myArg to 'myFunc' (string expected, got nil)" + }, + { name = 'checkTypeForNamedArg, other function', func = checkTypeForNamedArg, type='ToString', + args = { 'otherFunc', 'myArg', 9, 'string' }, + expect = "bad named argument myArg to 'otherFunc' (string expected, got number)" + }, + { name = 'checkTypeForNamedArg, other argument', func = checkTypeForNamedArg, type='ToString', + args = { 'myFunc', 'otherArg', 9, 'string' }, + expect = "bad named argument otherArg to 'myFunc' (string expected, got number)" + }, + + -- makeCheckSelfFunction + { name = 'makeCheckSelfFunction, valid', func = testCheckSelf, type='ToString', + args = { testObject, 'myMethod', 'myLibrary', 'myObject', testObject, 'test object' }, + expect = { nil } + }, + { name = 'makeCheckSelfFunction, invalid (1)', func = testCheckSelf, type='ToString', + args = { {}, 'myMethod', 'myLibrary', 'myObject', testObject, 'test object' }, + expect = 'myLibrary: invalid test object. Did you call myMethod with a dot instead ' .. + 'of a colon, i.e. myObject.myMethod() instead of myObject:myMethod()?' + }, + { name = 'makeCheckSelfFunction, invalid (2)', func = testCheckSelf, type='ToString', + args = { 'foo', 'myMethod', 'myLibrary', 'myObject', testObject, 'test object' }, + expect = 'myLibrary: invalid test object. Did you call myMethod with a dot instead ' .. + 'of a colon, i.e. myObject.myMethod() instead of myObject:myMethod()?' + }, + { name = 'makeCheckSelfFunction, other method', func = testCheckSelf, type='ToString', + args = { {}, 'otherMethod', 'myLibrary', 'myObject', testObject, 'test object' }, + expect = 'myLibrary: invalid test object. Did you call otherMethod with a dot instead ' .. + 'of a colon, i.e. myObject.otherMethod() instead of myObject:otherMethod()?' + }, + { name = 'makeCheckSelfFunction, other library', func = testCheckSelf, type='ToString', + args = { {}, 'myMethod', 'otherLibrary', 'myObject', testObject, 'test object' }, + expect = 'otherLibrary: invalid test object. Did you call myMethod with a dot instead ' .. + 'of a colon, i.e. myObject.myMethod() instead of myObject:myMethod()?' + }, + { name = 'makeCheckSelfFunction, other object', func = testCheckSelf, type='ToString', + args = { {}, 'myMethod', 'otherLibrary', 'otherObject', testObject, 'test object' }, + expect = 'otherLibrary: invalid test object. Did you call myMethod with a dot instead ' .. + 'of a colon, i.e. otherObject.myMethod() instead of otherObject:myMethod()?' + }, + { name = 'makeCheckSelfFunction, other description', func = testCheckSelf, type='ToString', + args = { {}, 'myMethod', 'myLibrary', 'myObject', testObject, 'test object' }, + expect = 'myLibrary: invalid test object. Did you call myMethod with a dot instead ' .. + 'of a colon, i.e. myObject.myMethod() instead of myObject:myMethod()?' + }, +} + +return testframework.getTestProvider( tests ) diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LuaDataProvider.php b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LuaDataProvider.php new file mode 100644 index 00000000..c679557b --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LuaDataProvider.php @@ -0,0 +1,53 @@ +<?php + +// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps +class Scribunto_LuaDataProvider implements Iterator { + protected $engine = null; + protected $exports = null; + protected $key = 1; + + public function __construct( $engine, $moduleName ) { + $this->engine = $engine; + $this->key = 1; + $module = $engine->fetchModuleFromParser( + Title::makeTitle( NS_MODULE, $moduleName ) + ); + if ( $module === null ) { + throw new Exception( "Failed to load module $moduleName" ); + } + // Calling executeModule with null isn't the best idea, since it brings + // the whole export table into PHP and throws away metatables and such, + // but for this use case, we don't have anything like that to worry about + $this->exports = $engine->executeModule( $module->getInitChunk(), null, null ); + } + + public function destroy() { + $this->engine = null; + $this->exports = null; + } + + public function rewind() { + $this->key = 1; + } + + public function valid() { + return $this->key <= $this->exports['count']; + } + + public function key() { + return $this->key; + } + + public function next() { + $this->key++; + } + + public function current() { + return $this->engine->getInterpreter()->callFunction( $this->exports['provide'], $this->key ); + } + + public function run( $key ) { + list( $ret ) = $this->engine->getInterpreter()->callFunction( $this->exports['run'], $key ); + return $ret; + } +} diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LuaEngineTestBase.php b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LuaEngineTestBase.php new file mode 100644 index 00000000..31aeff67 --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LuaEngineTestBase.php @@ -0,0 +1,291 @@ +<?php + +/** + * This is the subclass for Lua library tests. It will automatically run all + * tests against LuaSandbox and LuaStandalone. + * + * Most of the time, you'll only need to override the following: + * - $moduleName: Name of the module being tested + * - getTestModules(): Add a mapping from $moduleName to the file containing + * the code. + */ +// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps +abstract class Scribunto_LuaEngineTestBase extends MediaWikiLangTestCase { + private static $engineConfigurations = [ + 'LuaSandbox' => [ + 'memoryLimit' => 50000000, + 'cpuLimit' => 30, + 'allowEnvFuncs' => true, + 'maxLangCacheSize' => 30, + ], + 'LuaStandalone' => [ + 'errorFile' => null, + 'luaPath' => null, + 'memoryLimit' => 50000000, + 'cpuLimit' => 30, + 'allowEnvFuncs' => true, + 'maxLangCacheSize' => 30, + ], + ]; + + private static $staticEngineName = null; + private $engineName = null; + private $engine = null; + private $luaDataProvider = null; + + /** + * Name to display instead of the default + * @var string + */ + protected $luaTestName = null; + + /** + * Name of the module being tested + * @var string + */ + protected static $moduleName = null; + + /** + * Class to use for the data provider + * @var string + */ + protected static $dataProviderClass = 'Scribunto_LuaDataProvider'; + + /** + * Tests to skip. Associative array mapping test name to skip reason. + * @var array + */ + protected $skipTests = []; + + public function __construct( + $name = null, array $data = [], $dataName = '', $engineName = null + ) { + if ( $engineName === null ) { + $engineName = self::$staticEngineName; + } + $this->engineName = $engineName; + parent::__construct( $name, $data, $dataName ); + } + + public static function suite( $className ) { + return self::makeSuite( $className ); + } + + protected static function makeSuite( $className, $group = null ) { + $suite = new PHPUnit_Framework_TestSuite; + $suite->setName( $className ); + + $class = new ReflectionClass( $className ); + + foreach ( self::$engineConfigurations as $engineName => $opts ) { + if ( $group !== null && $group !== $engineName ) { + continue; + } + + try { + $parser = new Parser; + $parser->startExternalParse( Title::newMainPage(), new ParserOptions, Parser::OT_HTML, true ); + $engineClass = "Scribunto_{$engineName}Engine"; + $engine = new $engineClass( + self::$engineConfigurations[$engineName] + [ 'parser' => $parser ] + ); + $parser->scribunto_engine = $engine; + $engine->setTitle( $parser->getTitle() ); + $engine->getInterpreter(); + } catch ( Scribunto_LuaInterpreterNotFoundError $e ) { + $suite->addTest( + new Scribunto_LuaEngineTestSkip( + $className, "interpreter for $engineName is not available" + ), [ 'Lua', $engineName ] + ); + continue; + } + + // Work around PHPUnit breakage: the only straightforward way to + // get the data provider is to call + // PHPUnit_Util_Test::getProvidedData, but that instantiates the + // class without passing any parameters to the constructor. But we + // *need* that engine name. + self::$staticEngineName = $engineName; + + $engineSuite = new PHPUnit_Framework_TestSuite; + $engineSuite->setName( "$engineName: $className" ); + + foreach ( $class->getMethods() as $method ) { + if ( PHPUnit_Framework_TestSuite::isTestMethod( $method ) && $method->isPublic() ) { + $name = $method->getName(); + $groups = PHPUnit_Util_Test::getGroups( $className, $name ); + $groups[] = 'Lua'; + $groups[] = $engineName; + $groups = array_unique( $groups ); + + $data = PHPUnit_Util_Test::getProvidedData( $className, $name ); + if ( is_array( $data ) || $data instanceof Iterator ) { + // with @dataProvider + $dataSuite = new PHPUnit_Framework_TestSuite_DataProvider( + $className . '::' . $name + ); + foreach ( $data as $k => $v ) { + $dataSuite->addTest( + new $className( $name, $v, $k, $engineName ), + $groups + ); + } + $engineSuite->addTest( $dataSuite ); + } elseif ( $data === false ) { + // invalid @dataProvider + $engineSuite->addTest( new PHPUnit_Framework_Warning( + "The data provider specified for {$className}::$name is invalid." + ) ); + } else { + // no @dataProvider + $engineSuite->addTest( + new $className( $name, [], '', $engineName ), + $groups + ); + } + } + } + + $suite->addTest( $engineSuite ); + } + + return $suite; + } + + protected function tearDown() { + if ( $this->luaDataProvider ) { + $this->luaDataProvider->destroy(); + $this->luaDataProvider = null; + } + if ( $this->engine ) { + $this->engine->destroy(); + $this->engine = null; + } + parent::tearDown(); + } + + /** + * Get the title used for unit tests + * + * @return Title + */ + protected function getTestTitle() { + return Title::newMainPage(); + } + + /** + * @return ScribuntoEngineBase + */ + protected function getEngine() { + if ( !$this->engine ) { + $parser = new Parser; + $options = new ParserOptions; + $options->setTemplateCallback( [ $this, 'templateCallback' ] ); + $parser->startExternalParse( $this->getTestTitle(), $options, Parser::OT_HTML, true ); + $class = "Scribunto_{$this->engineName}Engine"; + $this->engine = new $class( + self::$engineConfigurations[$this->engineName] + [ 'parser' => $parser ] + ); + $parser->scribunto_engine = $this->engine; + $this->engine->setTitle( $parser->getTitle() ); + } + return $this->engine; + } + + public function templateCallback( $title, $parser ) { + if ( isset( $this->extraModules[$title->getFullText()] ) ) { + return [ + 'text' => $this->extraModules[$title->getFullText()], + 'finalTitle' => $title, + 'deps' => [] + ]; + } + + $modules = $this->getTestModules(); + foreach ( $modules as $name => $fileName ) { + $modTitle = Title::makeTitle( NS_MODULE, $name ); + if ( $modTitle->equals( $title ) ) { + return [ + 'text' => file_get_contents( $fileName ), + 'finalTitle' => $title, + 'deps' => [] + ]; + } + } + return Parser::statelessFetchTemplate( $title, $parser ); + } + + public function toString() { + // When running tests written in Lua, return a nicer representation in + // the failure message. + if ( $this->luaTestName ) { + return $this->engineName . ': ' . $this->luaTestName; + } + return $this->engineName . ': ' . parent::toString(); + } + + protected function getTestModules() { + return [ + 'TestFramework' => __DIR__ . '/TestFramework.lua', + ]; + } + + public function provideLuaData() { + if ( !$this->luaDataProvider ) { + $class = static::$dataProviderClass; + $this->luaDataProvider = new $class ( $this->getEngine(), static::$moduleName ); + } + return $this->luaDataProvider; + } + + /** + * @dataProvider provideLuaData + * @param string $key + * @param string $testName + * @param mixed $expected + */ + public function testLua( $key, $testName, $expected ) { + $this->luaTestName = static::$moduleName."[$key]: $testName"; + if ( isset( $this->skipTests[$testName] ) ) { + $this->markTestSkipped( $this->skipTests[$testName] ); + } else { + try { + $actual = $this->provideLuaData()->run( $key ); + } catch ( Scribunto_LuaError $ex ) { + if ( substr( $ex->getLuaMessage(), 0, 6 ) === 'SKIP: ' ) { + $this->markTestSkipped( substr( $ex->getLuaMessage(), 6 ) ); + } else { + throw $ex; + } + } + $this->assertSame( $expected, $actual ); + } + $this->luaTestName = null; + } +} + +// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps +class Scribunto_LuaEngineTestSkip extends PHPUnit\Framework\TestCase { + private $className = ''; + private $message = ''; + + public function __construct( $className = '', $message = '' ) { + $this->className = $className; + $this->message = $message; + parent::__construct( 'testDummy' ); + } + + public function testDummy() { + if ( $this->className ) { + $this->markTestSkipped( $this->message ); + } else { + // Dummy + $this->assertTrue( true ); + } + } + + public function toString() { + return $this->className; + } +} diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LuaEnvironmentComparisonTest.php b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LuaEnvironmentComparisonTest.php new file mode 100644 index 00000000..64172207 --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LuaEnvironmentComparisonTest.php @@ -0,0 +1,114 @@ +<?php + +/** + * @group Lua + * @group LuaSandbox + * @group LuaStandalone + */ +// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps +class Scribunto_LuaEnvironmentComparisonTest extends MediaWikiTestCase { + public $sandboxOpts = [ + 'memoryLimit' => 50000000, + 'cpuLimit' => 30, + 'allowEnvFuncs' => true, + ]; + public $standaloneOpts = [ + 'errorFile' => null, + 'luaPath' => null, + 'memoryLimit' => 50000000, + 'cpuLimit' => 30, + 'allowEnvFuncs' => true, + ]; + + protected $engines = []; + + private function makeEngine( $class, $opts ) { + $parser = new Parser; + $options = new ParserOptions; + $options->setTemplateCallback( [ $this, 'templateCallback' ] ); + $parser->startExternalParse( Title::newMainPage(), $options, Parser::OT_HTML, true ); + $engine = new $class ( [ 'parser' => $parser ] + $opts ); + $parser->scribunto_engine = $engine; + $engine->setTitle( $parser->getTitle() ); + $engine->getInterpreter(); + return $engine; + } + + protected function setUp() { + parent::setUp(); + + try { + $this->engines['LuaSandbox'] = $this->makeEngine( + 'Scribunto_LuaSandboxEngine', $this->sandboxOpts + ); + } catch ( Scribunto_LuaInterpreterNotFoundError $e ) { + $this->markTestSkipped( "LuaSandbox interpreter not available" ); + return; + } + + try { + $this->engines['LuaStandalone'] = $this->makeEngine( + 'Scribunto_LuaStandaloneEngine', $this->standaloneOpts + ); + } catch ( Scribunto_LuaInterpreterNotFoundError $e ) { + $this->markTestSkipped( "LuaStandalone interpreter not available" ); + return; + } + } + + protected function tearDown() { + foreach ( $this->engines as $engine ) { + $engine->destroy(); + } + $this->engines = []; + parent::tearDown(); + } + + private function getGlobalEnvironment( $engine ) { + static $script = <<<LUA + xxxseen = {} + function xxxGetTable( t ) + if xxxseen[t] then + return 'table' + end + local ret = {} + xxxseen[t] = ret + for k, v in pairs( t ) do + if k ~= '_G' and string.sub( k, 1, 3 ) ~= 'xxx' then + if type( v ) == 'table' then + ret[k] = xxxGetTable( v ) + elseif type( v ) == 'string' + or type( v ) == 'number' + or type( v ) == 'boolean' + or type( v ) == 'nil' + then + ret[k] = v + else + ret[k] = type( v ) + end + end + end + return ret + end + return xxxGetTable( _G ) +LUA; + $func = $engine->getInterpreter()->loadString( $script, 'script' ); + return $engine->getInterpreter()->callFunction( $func ); + } + + public function testGlobalEnvironment() { + // Grab the first engine as the "standard" + $firstEngine = reset( $this->engines ); + $firstName = key( $this->engines ); + $firstEnv = $this->getGlobalEnvironment( $firstEngine ); + + // Test all others against it + foreach ( $this->engines as $secondName => $secondEngine ) { + if ( $secondName !== $firstName ) { + $secondEnv = $this->getGlobalEnvironment( $secondEngine ); + $this->assertEquals( $firstEnv, $secondEnv, + "Environments for $firstName and $secondName are not equivalent" ); + } + } + } +} diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LuaInterpreterTest.php b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LuaInterpreterTest.php new file mode 100644 index 00000000..7ee38144 --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LuaInterpreterTest.php @@ -0,0 +1,164 @@ +<?php + +// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps +abstract class Scribunto_LuaInterpreterTest extends MediaWikiTestCase { + abstract protected function newInterpreter( $opts = [] ); + + protected function setUp() { + parent::setUp(); + try { + $this->newInterpreter(); + } catch ( Scribunto_LuaInterpreterNotFoundError $e ) { + $this->markTestSkipped( "interpreter not available" ); + } + } + + protected function getBusyLoop( $interpreter ) { + $chunk = $interpreter->loadString( ' + local args = {...} + local x, i + local s = string.rep("x", 1000000) + local n = args[1] + for i = 1, n do + x = x or string.find(s, "y", 1, true) + end', + 'busy' ); + return $chunk; + } + + /** @dataProvider provideRoundtrip */ + public function testRoundtrip( /*...*/ ) { + $args = func_get_args(); + $args = $this->normalizeOrder( $args ); + $interpreter = $this->newInterpreter(); + $passthru = $interpreter->loadString( 'return ...', 'passthru' ); + $finalArgs = $args; + array_unshift( $finalArgs, $passthru ); + $ret = call_user_func_array( [ $interpreter, 'callFunction' ], $finalArgs ); + $ret = $this->normalizeOrder( $ret ); + $this->assertSame( $args, $ret ); + } + + /** @dataProvider provideRoundtrip */ + public function testDoubleRoundtrip( /* ... */ ) { + $args = func_get_args(); + $args = $this->normalizeOrder( $args ); + + $interpreter = $this->newInterpreter(); + $interpreter->registerLibrary( 'test', + [ 'passthru' => [ $this, 'passthru' ] ] ); + $doublePassthru = $interpreter->loadString( + 'return test.passthru(...)', 'doublePassthru' ); + + $finalArgs = $args; + array_unshift( $finalArgs, $doublePassthru ); + $ret = call_user_func_array( [ $interpreter, 'callFunction' ], $finalArgs ); + $ret = $this->normalizeOrder( $ret ); + $this->assertSame( $args, $ret ); + } + + /** + * This cannot be done in testRoundtrip and testDoubleRoundtrip, because + * assertSame( NAN, NAN ) returns false. + */ + public function testRoundtripNAN() { + $interpreter = $this->newInterpreter(); + + $passthru = $interpreter->loadString( 'return ...', 'passthru' ); + $ret = $interpreter->callFunction( $passthru, NAN ); + $this->assertTrue( is_nan( $ret[0] ), 'NaN was not passed through' ); + + $interpreter->registerLibrary( 'test', + [ 'passthru' => [ $this, 'passthru' ] ] ); + $doublePassthru = $interpreter->loadString( + 'return test.passthru(...)', 'doublePassthru' ); + $ret = $interpreter->callFunction( $doublePassthru, NAN ); + $this->assertTrue( is_nan( $ret[0] ), 'NaN was not double passed through' ); + } + + private function normalizeOrder( $a ) { + ksort( $a ); + foreach ( $a as &$value ) { + if ( is_array( $value ) ) { + $value = $this->normalizeOrder( $value ); + } + } + return $a; + } + + public function passthru( /* ... */ ) { + $args = func_get_args(); + return $args; + } + + public function provideRoundtrip() { + return [ + [ 1 ], + [ true ], + [ false ], + [ 'hello' ], + [ implode( '', array_map( 'chr', range( 0, 255 ) ) ) ], + [ 1, 2, 3 ], + [ [] ], + [ [ 0 => 'foo', 1 => 'bar' ] ], + [ [ 1 => 'foo', 2 => 'bar' ] ], + [ [ 'x' => 'foo', 'y' => 'bar', 'z' => [] ] ], + [ INF ], + [ -INF ], + [ 'ok', null, 'ok' ], + [ null, 'ok' ], + [ 'ok', null ], + [ null ], + ]; + } + + public function testTimeLimit() { + if ( php_uname( 's' ) === 'Darwin' ) { + $this->markTestSkipped( "Darwin is lacking POSIX timer, skipping CPU time limiting test." ); + } + + $interpreter = $this->newInterpreter( [ 'cpuLimit' => 2 ] ); + $chunk = $this->getBusyLoop( $interpreter ); + try { + $interpreter->callFunction( $chunk, 1e9 ); + $this->fail( "Expected ScribuntoException was not thrown" ); + } catch ( ScribuntoException $ex ) { + $this->assertSame( 'scribunto-common-timeout', $ex->messageName ); + } + } + + public function testTestMemoryLimit() { + $interpreter = $this->newInterpreter( [ 'memoryLimit' => 20 * 1e6 ] ); + $chunk = $interpreter->loadString( ' + t = {} + for i = 1, 10 do + t[#t + 1] = string.rep("x" .. i, 1000000) + end + ', + 'memoryLimit' ); + try { + $interpreter->callFunction( $chunk ); + $this->fail( "Expected ScribuntoException was not thrown" ); + } catch ( ScribuntoException $ex ) { + $this->assertSame( 'scribunto-lua-error', $ex->messageName ); + $this->assertSame( 'not enough memory', $ex->messageArgs[1] ); + } + } + + public function testWrapPHPFunction() { + $interpreter = $this->newInterpreter(); + $func = $interpreter->wrapPhpFunction( function ( $n ) { + return [ 42, $n ]; + } ); + $res = $interpreter->callFunction( $func, 'From PHP' ); + $this->assertEquals( [ 42, 'From PHP' ], $res ); + + $chunk = $interpreter->loadString( ' + f = ... + return f( "From Lua" ) + ', + 'wrappedPhpFunction' ); + $res = $interpreter->callFunction( $chunk, $func ); + $this->assertEquals( [ 42, 'From Lua' ], $res ); + } +} diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/MessageLibraryTest.php b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/MessageLibraryTest.php new file mode 100644 index 00000000..fdfc9a67 --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/MessageLibraryTest.php @@ -0,0 +1,12 @@ +<?php + +// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps +class Scribunto_LuaMessageLibraryTest extends Scribunto_LuaEngineTestBase { + protected static $moduleName = 'MessageLibraryTests'; + + protected function getTestModules() { + return parent::getTestModules() + [ + 'MessageLibraryTests' => __DIR__ . '/MessageLibraryTests.lua', + ]; + } +} diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/MessageLibraryTests.lua b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/MessageLibraryTests.lua new file mode 100644 index 00000000..55dafb8f --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/MessageLibraryTests.lua @@ -0,0 +1,70 @@ +local testframework = require 'Module:TestFramework' + +local message1 = mw.message.new( 'mainpage' ) +local message1_copy = mw.message.new( 'mainpage' ) +local message2 = mw.message.new( 'i-dont-exist-evar' ) + +function test_exists( key ) + return mw.message.new( key ):exists() +end + +function test_language( key ) + -- If mw.language is available, test that too + local lang = 'ru' + if mw.language then + lang = mw.language.new( 'ru' ) + end + + return mw.message.new( 'mainpage' ):useDatabase( false ):inLanguage( 'en' ):plain(), + mw.message.new( 'mainpage' ):useDatabase( false ):inLanguage( 'ru' ):plain(), + mw.message.new( 'mainpage' ):useDatabase( false ):inLanguage( lang ):plain() +end + +function test_params( rawMessage, func, ... ) + local msg = mw.message.newRawMessage( rawMessage ):inLanguage( 'en' ) + return msg[func]( msg, ... ):plain() +end + +return testframework.getTestProvider( { + { name = 'exists (1)', func = test_exists, + args = { 'mainpage' }, + expect = { true } + }, + { name = 'exists (2)', func = test_exists, + args = { 'i-dont-exist-evar' }, + expect = { false } + }, + + { name = 'inLanguage', func = test_language, + expect = { 'Main Page', 'Заглавная страница', 'Заглавная страница' } + }, + + { name = 'plain param', func = test_params, + args = { '($1 $2)', 'params', "'''foo'''", 123456 }, + expect = { "('''foo''' 123456)" } + }, + { name = 'raw param', func = test_params, + args = { '($1 $2)', 'rawParams', "'''foo'''", 123456 }, + expect = { "('''foo''' 123456)" } + }, + { name = 'num param', func = test_params, + args = { '($1 $2)', 'numParams', "'''foo'''", 123456 }, + expect = { "('''foo''' 123,456)" } + }, + { name = 'mixed params', func = test_params, + args = { '($1 $2 $3)', 'params', + "'''foo'''", mw.message.rawParam( "'''foo'''" ), mw.message.numParam( 123456 ) + }, + expect = { "('''foo''' '''foo''' 123,456)" } + }, + + { name = 'message as param', func = test_params, + args = { '($1)', 'params', mw.message.newRawMessage( 'bar' ) }, + expect = { "(bar)" } + }, + + { name = 'different title', func = test_params, + args = { '($1)', 'params', mw.message.newRawMessage( 'bar' ) }, + expect = { "(bar)" } + }, +} ) diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/SiteLibraryTest.php b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/SiteLibraryTest.php new file mode 100644 index 00000000..36a6dc74 --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/SiteLibraryTest.php @@ -0,0 +1,12 @@ +<?php + +// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps +class Scribunto_LuaSiteLibraryTest extends Scribunto_LuaEngineTestBase { + protected static $moduleName = 'SiteLibraryTests'; + + protected function getTestModules() { + return parent::getTestModules() + [ + 'SiteLibraryTests' => __DIR__ . '/SiteLibraryTests.lua', + ]; + } +} diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/SiteLibraryTests.lua b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/SiteLibraryTests.lua new file mode 100644 index 00000000..4b9625c3 --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/SiteLibraryTests.lua @@ -0,0 +1,182 @@ +local testframework = require 'Module:TestFramework' + +local function nsTest( ... ) + local args = { ... } + local t = mw.site.namespaces + local path = 'mw.site.namespaces' + for i = 1, #args do + t = t[args[i]] + path = path .. string.format( '[%q]', args[i] ) + if t == nil then + error( path .. ' is nil!' ) + end + end + return t +end + +local function isNonEmptyString( val ) + return type( val ) == 'string' and val ~= '' +end + +local function isValidInterwikiMap( map ) + assert( type( map ) == 'table', "mw.site.interwikiMap did not return a table" ) + local stringKeys = { 'prefix', 'url' } + local boolKeys = { + 'isProtocolRelative', + 'isLocal', + 'isTranscludable', + 'isCurrentWiki', + 'isExtraLanguageLink' + } + local maybeStringKeys = { 'displayText', 'tooltip' } + for prefix, data in pairs( map ) do + for _, key in ipairs( stringKeys ) do + assert( isNonEmptyString( data[key] ), + key .. " is not a string or is the empty string" + ) + end + assert( prefix == data.prefix, string.format( + "table key '%s' and prefix '%s' do not match", + tostring( prefix ), tostring( data.prefix ) + ) ) + for _, key in ipairs( boolKeys ) do + assert( type( data[key] ) == 'boolean', key .. " is not a boolean" ) + end + for _, key in ipairs( maybeStringKeys ) do + assert( data[key] == nil or isNonEmptyString( data[key] ), + key .. " is not a string or is the empty string, and is not nil" + ) + end + end + return true +end + +return testframework.getTestProvider( { + { name = 'parameter: siteName', + func = type, args = { mw.site.siteName }, + expect = { 'string' } + }, + { name = 'parameter: server', + func = type, args = { mw.site.server }, + expect = { 'string' } + }, + { name = 'parameter set: scriptPath', + func = type, args = { mw.site.scriptPath }, + expect = { 'string' } + }, + + { name = 'parameter set: stats.pages', + func = type, args = { mw.site.stats.pages }, + expect = { 'number' } + }, + + { name = 'pagesInCategory', + func = type, args = { mw.site.stats.pagesInCategory( "Example" ) }, + expect = { 'number' } + }, + + { name = 'pagesInNamespace', + func = type, args = { mw.site.stats.pagesInNamespace( 0 ) }, + expect = { 'number' } + }, + + { name = 'usersInGroup', + func = type, args = { mw.site.stats.usersInGroup( 'sysop' ) }, + expect = { 'number' } + }, + + { name = 'Project namespace by number', + func = nsTest, args = { 4, 'canonicalName' }, + expect = { 'Project' } + }, + + { name = 'Project namespace by name', + func = nsTest, args = { 'Project', 'id' }, + expect = { 4 } + }, + + { name = 'Project namespace by name (2)', + func = nsTest, args = { 'PrOjEcT', 'canonicalName' }, + expect = { 'Project' } + }, + + { name = 'Project namespace subject is itself', + func = nsTest, args = { 'Project', 'subject', 'canonicalName' }, + expect = { 'Project' } + }, + + { name = 'Project talk namespace via Project', + func = nsTest, args = { 'Project', 'talk', 'canonicalName' }, + expect = { 'Project talk' } + }, + + { name = 'Project namespace via Project talk', + func = nsTest, args = { 'Project_talk', 'subject', 'canonicalName' }, + expect = { 'Project' } + }, + + { name = 'Project talk namespace via Project (associated)', + func = nsTest, args = { 'Project', 'associated', 'canonicalName' }, + expect = { 'Project talk' } + }, + + { name = 'Project talk namespace by name (standard caps, no underscores)', + func = nsTest, args = { 'Project talk', 'id' }, + expect = { 5 } + }, + + { name = 'Project talk namespace by name (standard caps, underscores)', + func = nsTest, args = { 'Project_talk', 'id' }, + expect = { 5 } + }, + + { name = 'Project talk namespace by name (odd caps, no underscores)', + func = nsTest, args = { 'pRoJeCt tAlK', 'id' }, + expect = { 5 } + }, + + { name = 'Project talk namespace by name (odd caps, underscores)', + func = nsTest, args = { 'pRoJeCt_tAlK', 'id' }, + expect = { 5 } + }, + + { name = 'Project talk namespace by name (extraneous spaces and underscores)', + func = nsTest, args = { '_ _ _Project_ _talk_ _ _', 'id' }, + expect = { 5 } + }, + + { name = 'interwikiMap (all prefixes)', + func = isValidInterwikiMap, args = { mw.site.interwikiMap() }, + expect = { true } + }, + + { name = 'interwikiMap (local prefixes)', + func = isValidInterwikiMap, args = { mw.site.interwikiMap( 'local' ) }, + expect = { true } + }, + + { name = 'interwikiMap (non-local prefixes)', + func = isValidInterwikiMap, args = { mw.site.interwikiMap( '!local' ) }, + expect = { true } + }, + + { name = 'interwikiMap (type error 1)', + func = mw.site.interwikiMap, args = { 123 }, + expect = "bad argument #1 to 'interwikiMap' (string expected, got number)" + }, + + { name = 'interwikiMap (type error 2)', + func = mw.site.interwikiMap, args = { false }, + expect = "bad argument #1 to 'interwikiMap' (string expected, got boolean)" + }, + + { name = 'interwikiMap (unknown filter 1)', + func = mw.site.interwikiMap, args = { '' }, + expect = "bad argument #1 to 'interwikiMap' (unknown filter '')" + }, + + { name = 'interwikiMap (unknown filter 2)', + func = mw.site.interwikiMap, args = { 'foo' }, + expect = "bad argument #1 to 'interwikiMap' (unknown filter 'foo')" + }, +} ) diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/TestFramework.lua b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/TestFramework.lua new file mode 100644 index 00000000..6231695a --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/TestFramework.lua @@ -0,0 +1,246 @@ +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 diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/TextLibraryTest.php b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/TextLibraryTest.php new file mode 100644 index 00000000..e444dc05 --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/TextLibraryTest.php @@ -0,0 +1,41 @@ +<?php + +// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps +class Scribunto_LuaTextLibraryTest extends Scribunto_LuaEngineTestBase { + protected static $moduleName = 'TextLibraryTests'; + + public function __construct( + $name = null, array $data = [], $dataName = '', $engineName = null + ) { + parent::__construct( $name, $data, $dataName, $engineName ); + if ( defined( 'HHVM_VERSION' ) ) { + // HHVM bug https://github.com/facebook/hhvm/issues/5813 + $this->skipTests['json decode, invalid values (trailing comma)'] = + 'json decode bug in HHVM'; + } + } + + protected function setUp() { + parent::setUp(); + + // For unstrip test + $parser = $this->getEngine()->getParser(); + $markers = [ + 'nowiki' => Parser::MARKER_PREFIX . '-test-nowiki-' . Parser::MARKER_SUFFIX, + 'general' => Parser::MARKER_PREFIX . '-test-general-' . Parser::MARKER_SUFFIX, + ]; + $parser->mStripState->addNoWiki( $markers['nowiki'], 'NoWiki' ); + $parser->mStripState->addGeneral( $markers['general'], 'General' ); + $interpreter = $this->getEngine()->getInterpreter(); + $interpreter->callFunction( + $interpreter->loadString( 'mw.text.stripTest = ...', 'fortest' ), + $markers + ); + } + + protected function getTestModules() { + return parent::getTestModules() + [ + 'TextLibraryTests' => __DIR__ . '/TextLibraryTests.lua', + ]; + } +} diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/TextLibraryTests.lua b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/TextLibraryTests.lua new file mode 100644 index 00000000..85dd9e34 --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/TextLibraryTests.lua @@ -0,0 +1,509 @@ +local testframework = require 'Module:TestFramework' + +-- Force the argument list to be ordered +local tagattrs = { absent = false, present = true, key = 'value', n = 42 } +setmetatable( tagattrs, { __pairs = function ( t ) + local keys = { 'absent', 'present', 'key', 'n' } + local i = 0 + return function() + i = i + 1 + if i <= #keys then + return keys[i], t[keys[i]] + end + end +end } ) + +-- For data provider, make sure this is defined +mw.text.stripTest = mw.text.stripTest or { nowiki = '!!!', general = '!!!' } + +-- Can't directly expect the value from mw.text.stripTest, because when +-- 'expect' is processed by the data provider it's the dummy entry above. +local function stripTest( func, marker ) + local result = func( marker ) + if result == marker then + result = 'strip-marker' + end + return result +end + +-- Round-trip test for json encode/decode, mainly because we can't rely on +-- order when encoding multi-element objects. +function jsonRoundTripTest( tree ) + return mw.text.jsonDecode( mw.text.jsonEncode( tree ) ) +end + +local recursiveTable = {} +recursiveTable.recursiveTable = recursiveTable + +-- Tests +local tests = { + { name = 'trim', + func = mw.text.trim, args = { ' foo bar ' }, + expect = { 'foo bar' } + }, + { name = 'trim right', + func = mw.text.trim, args = { 'foo bar ' }, + expect = { 'foo bar' } + }, + { name = 'trim left', + func = mw.text.trim, args = { ' foo bar' }, + expect = { 'foo bar' } + }, + { name = 'trim none', + func = mw.text.trim, args = { 'foo bar' }, + expect = { 'foo bar' } + }, + { name = 'trim charset', + func = mw.text.trim, args = { 'xxx foo bar xxx', 'x' }, + expect = { ' foo bar ' } + }, + + { name = 'encode', + func = mw.text.encode, args = { '<b>foo\194\160"bar"</b> & \'baz\'' }, + expect = { '<b>foo "bar"</b> & 'baz'' } + }, + { name = 'encode charset', + func = mw.text.encode, args = { '<b>foo\194\160"bar"</b> & \'baz\'', 'aeiou' }, + expect = { '<b>foo\194\160"bar"</b> & \'baz\'' } + }, + + { name = 'decode', + func = mw.text.decode, + args = { '<>&" foo foo ♥ &quot;' }, + expect = { '<>&" foo foo ♥ "' } + }, + { name = 'decode named', + func = mw.text.decode, + args = { '<>&" foo foo ♥ &quot;', true }, + expect = { '<>&" foo foo ♥ "' } + }, + + { name = 'nowiki', + func = mw.text.nowiki, + args = { '*"&\'<=>[]{|}#*:;\n*\n#\n:\n;\nhttp://example.com:80/\nRFC 123, ISBN 456' }, + expect = { + '*"&'<=>[]{|}#*:;' .. + '\n*\n#\n:\n;\nhttp://example.com:80/' .. + '\nRFC 123, ISBN 456' + } + }, + + { name = 'tag, simple', + func = mw.text.tag, + args = { { name = 'b' } }, + expect = { '<b>' } + }, + { name = 'tag, simple with content', + func = mw.text.tag, + args = { { name = 'b', content = 'foo' } }, + expect = { '<b>foo</b>' } + }, + { name = 'tag, simple self-closing', + func = mw.text.tag, + args = { { name = 'br', content = false } }, + expect = { '<br />' } + }, + { name = 'tag, args', + func = mw.text.tag, + args = { { name = 'b', attrs = tagattrs } }, + expect = { '<b present key="value" n="42">' } + }, + { name = 'tag, args with content', + func = mw.text.tag, + args = { { name = 'b', attrs = tagattrs, content = 'foo' } }, + expect = { '<b present key="value" n="42">foo</b>' } + }, + { name = 'tag, args self-closing', + func = mw.text.tag, + args = { { name = 'br', attrs = tagattrs, content = false } }, + expect = { '<br present key="value" n="42" />' } + }, + { name = 'tag, args, positional params', + func = mw.text.tag, + args = { 'b', tagattrs }, + expect = { '<b present key="value" n="42">' } + }, + { name = 'tag, args with content, positional params', + func = mw.text.tag, + args = { 'b', tagattrs, 'foo' }, + expect = { '<b present key="value" n="42">foo</b>' } + }, + + { name = 'unstrip (nowiki)', + func = stripTest, + args = { mw.text.unstrip, mw.text.stripTest.nowiki }, + expect = { 'NoWiki' } + }, + { name = 'unstrip (general)', + func = stripTest, + args = { mw.text.unstrip, mw.text.stripTest.general }, + expect = { '' } + }, + + { name = 'unstripNoWiki (nowiki)', + func = stripTest, + args = { mw.text.unstripNoWiki, mw.text.stripTest.nowiki }, + expect = { 'NoWiki' } + }, + { name = 'unstripNoWiki (general)', + func = stripTest, + args = { mw.text.unstripNoWiki, mw.text.stripTest.general }, + expect = { 'strip-marker' } + }, + + { name = 'killMarkers', + func = mw.text.killMarkers, + args = { 'a' .. mw.text.stripTest.nowiki .. 'b' .. mw.text.stripTest.general .. 'c' }, + expect = { 'abc' } + }, + + { name = 'split, simple', + func = mw.text.split, args = { 'a,b,c,d', ',' }, + expect = { { 'a', 'b', 'c', 'd' } } + }, + { name = 'split, no separator', + func = mw.text.split, args = { 'xxx', ',' }, + expect = { { 'xxx' } } + }, + { name = 'split, empty string', + func = mw.text.split, args = { '', ',' }, + expect = { { '' } } + }, + { name = 'split, with empty items', + func = mw.text.split, args = { ',,', ',' }, + expect = { { '', '', '' } } + }, + { name = 'split, with empty items (1)', + func = mw.text.split, args = { 'x,,', ',' }, + expect = { { 'x', '', '' } } + }, + { name = 'split, with empty items (2)', + func = mw.text.split, args = { ',x,', ',' }, + expect = { { '', 'x', '' } } + }, + { name = 'split, with empty items (3)', + func = mw.text.split, args = { ',,x', ',' }, + expect = { { '', '', 'x' } } + }, + { name = 'split, with empty items (4)', + func = mw.text.split, args = { ',x,x', ',' }, + expect = { { '', 'x', 'x' } } + }, + { name = 'split, with empty items (5)', + func = mw.text.split, args = { 'x,,x', ',' }, + expect = { { 'x', '', 'x' } } + }, + { name = 'split, with empty items (7)', + func = mw.text.split, args = { 'x,x,', ',' }, + expect = { { 'x', 'x', '' } } + }, + { name = 'split, with empty pattern', + func = mw.text.split, args = { 'xxx', '' }, + expect = { { 'x', 'x', 'x' } } + }, + { name = 'split, with empty pattern (2)', + func = mw.text.split, args = { 'xxx', ',?' }, + expect = { { 'x', 'x', 'x' } } + }, + + { name = 'listToText (0)', + func = mw.text.listToText, args = { {} }, + expect = { '' } + }, + { name = 'listToText (1)', + func = mw.text.listToText, args = { { 1 } }, + expect = { '1' } + }, + { name = 'listToText (2)', + func = mw.text.listToText, args = { { 1, 2 } }, + expect = { '1 and 2' } + }, + { name = 'listToText (3)', + func = mw.text.listToText, args = { { 1, 2, 3 } }, + expect = { '1, 2 and 3' } + }, + { name = 'listToText (4)', + func = mw.text.listToText, args = { { 1, 2, 3, 4 } }, + expect = { '1, 2, 3 and 4' } + }, + { name = 'listToText, alternate separator', + func = mw.text.listToText, args = { { 1, 2, 3, 4 }, '; ' }, + expect = { '1; 2; 3 and 4' } + }, + { name = 'listToText, alternate conjunction', + func = mw.text.listToText, args = { { 1, 2, 3, 4 }, nil, ' or ' }, + expect = { '1, 2, 3 or 4' } + }, + + { name = 'truncate, no truncation', + func = mw.text.truncate, args = { 'foobarbaz', 9 }, + expect = { 'foobarbaz' } + }, + { name = 'truncate, no truncation (2)', + func = mw.text.truncate, args = { 'foobarbaz', -9 }, + expect = { 'foobarbaz' } + }, + { name = 'truncate, tail truncation', + func = mw.text.truncate, args = { 'foobarbaz', 3 }, + expect = { 'foo...' } + }, + { name = 'truncate, head truncation', + func = mw.text.truncate, args = { 'foobarbaz', -3 }, + expect = { '...baz' } + }, + { name = 'truncate, avoid silly truncation', + func = mw.text.truncate, args = { 'foobarbaz', 8 }, + expect = { 'foobarbaz' } + }, + { name = 'truncate, avoid silly truncation (2)', + func = mw.text.truncate, args = { 'foobarbaz', 6 }, + expect = { 'foobarbaz' } + }, + { name = 'truncate, alternate ellipsis', + func = mw.text.truncate, args = { 'foobarbaz', 3, '!' }, + expect = { 'foo!' } + }, + { name = 'truncate, with adjusted length', + func = mw.text.truncate, args = { 'foobarbaz', 6, nil, true }, + expect = { 'foo...' } + }, + { name = 'truncate, with adjusted length (2)', + func = mw.text.truncate, args = { 'foobarbaz', -6, nil, true }, + expect = { '...baz' } + }, + { name = 'truncate, ridiculously short', + func = mw.text.truncate, args = { 'foobarbaz', 1, nil, true }, + expect = { '...' } + }, + { name = 'truncate, ridiculously short (2)', + func = mw.text.truncate, args = { 'foobarbaz', -1, nil, true }, + expect = { '...' } + }, + + { name = 'json encode-decode round trip, simple object', + func = jsonRoundTripTest, + args = { { + int = 2, + string = "foo", + ['true'] = true, + ['false'] = false, + } }, + expect = { { + int = 2, + string = "foo", + ['true'] = true, + ['false'] = false, + } }, + }, + { name = 'json decode, simple object', + func = mw.text.jsonDecode, + args = { '{"int":2,"string":"foo","true":true,"false":false}' }, + expect = { { + int = 2, + string = "foo", + ['true'] = true, + ['false'] = false, + } }, + }, + { name = 'json encode, simple array', + func = mw.text.jsonEncode, + args = { { 1, "foo", true, false } }, + expect = { '[1,"foo",true,false]' } + }, + { name = 'json decode, simple array', + func = mw.text.jsonDecode, + args = { '[1,"foo",true,false]' }, + expect = { { 1, "foo", true, false } } + }, + { name = 'json encode-decode round trip, object with numeric keys', + func = jsonRoundTripTest, + args = { { x = "x", [1] = 1, [2] = 2 } }, + expect = { { x = "x", [1] = 1, [2] = 2 } } + }, + { name = 'json decode, object with numeric keys', + func = mw.text.jsonDecode, + args = { '{"x":"x","1":1,"2":2}' }, + expect = { { x = "x", [1] = 1, [2] = 2 } } + }, + { name = 'json encode, simple array, preserve keys', + func = mw.text.jsonEncode, + args = { { 1, "foo", true, false }, mw.text.JSON_PRESERVE_KEYS }, + expect = { '{"1":1,"2":"foo","3":true,"4":false}' } + }, + { name = 'json decode, simple array, preserve keys', + func = mw.text.jsonDecode, + args = { '[1,"foo",true,false]', mw.text.JSON_PRESERVE_KEYS }, + expect = { { [0] = 1, "foo", true, false } } + }, + { name = 'json encode, nested arrays', + func = mw.text.jsonEncode, + args = { { 1, 2, 3, { 4, 5, { 6, 7, 8 } } } }, + expect = { '[1,2,3,[4,5,[6,7,8]]]' } + }, + { name = 'json decode, nested arrays', + func = mw.text.jsonDecode, + args = { '[1,2,3,[4,5,[6,7,8]]]' }, + expect = { { 1, 2, 3, { 4, 5, { 6, 7, 8 } } } } + }, + { name = 'json encode, array in object', + func = mw.text.jsonEncode, + args = { { x = { 1, 2, { y = { 3, 4 } } } } }, + expect = { '{"x":[1,2,{"y":[3,4]}]}' } + }, + { name = 'json decode, array in object', + func = mw.text.jsonDecode, + args = { '{"x":[1,2,{"y":[3,4]}],"z":[5,6]}' }, + expect = { { x = { 1, 2, { y = { 3, 4 } } }, z = { 5, 6 } } } + }, + { name = 'json decode, empty array', + func = mw.text.jsonDecode, + args = { '[]' }, + expect = { {} } + }, + { name = 'json decode, empty object', + func = mw.text.jsonDecode, + args = { '{}' }, + expect = { {} } + }, + { name = 'json encode, object with one large numeric index', + func = mw.text.jsonEncode, + args = { { [1000] = 1 } }, + expect = { '{"1000":1}' } + }, + { name = 'json decode, object with one large numeric index', + func = mw.text.jsonDecode, + args = { '{"1000":1}' }, + expect = { { [1000] = 1 } } + }, + { name = 'json encode, array with holes (ideally would be "[1,2,nil,4]", but probably not worth worrying about)', + func = mw.text.jsonEncode, + args = { { 1, 2, nil, 4 } }, + expect = { '{"1":1,"2":2,"4":4}' } + }, + { name = 'json decode, array with null (ideally would somehow insist on having a [3] = nil element, but that\'s not easily possible)', + func = mw.text.jsonDecode, + args = { '[1,2,null,4]' }, + expect = { { 1, 2, [4] = 4 } } + }, + { name = 'json encode, empty table (could be either [] or {}, but change should be announced)', + func = mw.text.jsonEncode, + args = { {} }, + expect = { '[]' } + }, + { name = 'json encode, table with index 0 (technically wrong, but probably not worth working around)', + func = mw.text.jsonEncode, + args = { { [0] = "zero" } }, + expect = { '["zero"]' } + }, + { name = 'json decode, object with index 1 (technically wrong, but probably not worth working around)', + func = mw.text.jsonDecode, + args = { '{"1":"one"}' }, + expect = { { 'one' } } + }, + { name = 'json encode, pretty', + func = mw.text.jsonEncode, + args = { { 1, 2, 3, { 4, 5, { 6, 7, { x = 8 } } } }, mw.text.JSON_PRETTY }, + expect = { [=[[ + 1, + 2, + 3, + [ + 4, + 5, + [ + 6, + 7, + { + "x": 8 + } + ] + ] +]]=] } + }, + { name = 'json encode, raw value (technically not allowed, but a common extension)', + func = mw.text.jsonEncode, + args = { "foo" }, + expect = { '"foo"' } + }, + { name = 'json decode, raw value (technically not allowed, but a common extension)', + func = mw.text.jsonDecode, + args = { '"foo"' }, + expect = { 'foo' } + }, + { name = 'json encode, sneaky nil injection (object)', + func = mw.text.jsonEncode, + args = { setmetatable( {}, { + __pairs = function ( t ) + return function ( t, k ) + if k ~= "foo" then + return "foo", nil + end + end, t, nil + end, + } ) }, + expect = { '{"foo":null}' } + }, + { name = 'json encode, sneaky nil injection (array)', + func = mw.text.jsonEncode, + args = { setmetatable( { "one", "two", nil, "four" }, { + __pairs = function ( t ) + return function ( t, k ) + k = k and k + 1 or 1 + if k <= 4 then + return k, t[k] + end + end, t, nil + end, + } ) }, + expect = { '["one","two",null,"four"]' } + }, + + { name = 'json encode, invalid values (inf)', + func = mw.text.jsonEncode, + args = { { 1/0 } }, + expect = 'mw.text.jsonEncode: Cannot encode non-finite numbers' + }, + { name = 'json encode, invalid values (nan)', + func = mw.text.jsonEncode, + args = { { 0/0 } }, + expect = 'mw.text.jsonEncode: Cannot encode non-finite numbers' + }, + { name = 'json encode, invalid values (function)', + func = mw.text.jsonEncode, + args = { { function () end } }, + expect = 'mw.text.jsonEncode: Cannot encode type \'function\'' + }, + { name = 'json encode, invalid values (recursive table)', + func = mw.text.jsonEncode, + args = { { recursiveTable } }, + expect = 'mw.text.jsonEncode: Cannot use recursive tables' + }, + { name = 'json encode, invalid values (table with bool key)', + func = mw.text.jsonEncode, + args = { { [true] = 1 } }, + expect = 'mw.text.jsonEncode: Cannot use type \'boolean\' as a table key' + }, + { name = 'json encode, invalid values (table with function key)', + func = mw.text.jsonEncode, + args = { { [function() end] = 1 } }, + expect = 'mw.text.jsonEncode: Cannot use type \'function\' as a table key' + }, + { name = 'json encode, invalid values (table with inf key)', + func = mw.text.jsonEncode, + args = { { [1/0] = 1 } }, + expect = 'mw.text.jsonEncode: Cannot use \'inf\' as a table key' + }, + + { name = 'json decode, invalid values (trailing comma)', + func = mw.text.jsonDecode, + args = { '{"x":1,}' }, + expect = 'mw.text.jsonDecode: Syntax error' + }, + { name = 'json decode, trailing comma with JSON_TRY_FIXING', + func = mw.text.jsonDecode, + args = { '{"x":1,}', mw.text.JSON_TRY_FIXING }, + expect = { { x = 1 } } + }, +} + +return testframework.getTestProvider( tests ) diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/TitleLibraryTest.php b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/TitleLibraryTest.php new file mode 100644 index 00000000..14c97d0f --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/TitleLibraryTest.php @@ -0,0 +1,167 @@ +<?php + +// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps +class Scribunto_LuaTitleLibraryTest extends Scribunto_LuaEngineTestBase { + protected static $moduleName = 'TitleLibraryTests'; + + public static function suite( $className ) { + global $wgInterwikiCache; + if ( $wgInterwikiCache ) { + $suite = new PHPUnit_Framework_TestSuite; + $suite->setName( $className ); + $suite->addTest( + new Scribunto_LuaEngineTestSkip( + $className, 'Cannot run TitleLibrary tests when $wgInterwikiCache is set' + ), [ 'Lua' ] + ); + return $suite; + } + + return parent::suite( $className ); + } + + protected function setUp() { + global $wgHooks; + + parent::setUp(); + + // Hook to inject our interwiki prefix + $this->hooks = $wgHooks; + $wgHooks['InterwikiLoadPrefix'][] = function ( $prefix, &$data ) { + if ( $prefix !== 'interwikiprefix' ) { + return true; + } + + $data = [ + 'iw_prefix' => 'interwikiprefix', + 'iw_url' => '//test.wikipedia.org/wiki/$1', + 'iw_api' => 1, + 'iw_wikiid' => 0, + 'iw_local' => 0, + 'iw_trans' => 0, + ]; + return false; + }; + + // Page for getContent test + $page = WikiPage::factory( Title::newFromText( 'ScribuntoTestPage' ) ); + $page->doEditContent( + new WikitextContent( + '{{int:mainpage}}<includeonly>...</includeonly><noinclude>...</noinclude>' + ), + 'Summary' + ); + $testPageId = $page->getId(); + + // Pages for redirectTarget tests + $page = WikiPage::factory( Title::newFromText( 'ScribuntoTestRedirect' ) ); + $page->doEditContent( + new WikitextContent( '#REDIRECT [[ScribuntoTestTarget]]' ), + 'Summary' + ); + $page = WikiPage::factory( Title::newFromText( 'ScribuntoTestNonRedirect' ) ); + $page->doEditContent( + new WikitextContent( 'Not a redirect.' ), + 'Summary' + ); + + // Set restrictions for protectionLevels and cascadingProtection tests + // Since mRestrictionsLoaded is true, they don't count as expensive + $title = Title::newFromText( 'Main Page' ); + $title->mRestrictionsLoaded = true; + $title->mRestrictions = [ 'edit' => [], 'move' => [] ]; + $title->mCascadeSources = [ + Title::makeTitle( NS_MAIN, "Lockbox" ), + Title::makeTitle( NS_MAIN, "Lockbox2" ), + ]; + $title->mCascadingRestrictions = [ 'edit' => [ 'sysop' ] ]; + $title = Title::newFromText( 'Module:TestFramework' ); + $title->mRestrictionsLoaded = true; + $title->mRestrictions = [ + 'edit' => [ 'sysop', 'bogus' ], + 'move' => [ 'sysop', 'bogus' ], + ]; + $title->mCascadeSources = []; + $title->mCascadingRestrictions = []; + $title = Title::newFromText( 'interwikiprefix:Module:TestFramework' ); + $title->mRestrictionsLoaded = true; + $title->mRestrictions = []; + $title->mCascadeSources = []; + $title->mCascadingRestrictions = []; + $title = Title::newFromText( 'Talk:Has/A/Subpage' ); + $title->mRestrictionsLoaded = true; + $title->mRestrictions = [ 'create' => [ 'sysop' ] ]; + $title->mCascadeSources = []; + $title->mCascadingRestrictions = []; + $title = Title::newFromText( 'Not/A/Subpage' ); + $title->mRestrictionsLoaded = true; + $title->mRestrictions = [ 'edit' => [ 'autoconfirmed' ], 'move' => [ 'sysop' ] ]; + $title->mCascadeSources = []; + $title->mCascadingRestrictions = []; + $title = Title::newFromText( 'Module talk:Test Framework' ); + $title->mRestrictionsLoaded = true; + $title->mRestrictions = [ 'edit' => [], 'move' => [ 'sysop' ] ]; + $title->mCascadeSources = []; + $title->mCascadingRestrictions = []; + + // Note this depends on every iteration of the data provider running with a clean parser + $this->getEngine()->getParser()->getOptions()->setExpensiveParserFunctionLimit( 10 ); + + // Indicate to the tests that it's safe to create the title objects + $interpreter = $this->getEngine()->getInterpreter(); + $interpreter->callFunction( + $interpreter->loadString( "mw.title.testPageId = $testPageId", 'fortest' ) + ); + + $this->setMwGlobals( [ + 'wgServer' => '//wiki.local', + 'wgCanonicalServer' => 'http://wiki.local', + 'wgUsePathInfo' => true, + 'wgActionPaths' => [], + 'wgScript' => '/w/index.php', + 'wgScriptPath' => '/w', + 'wgArticlePath' => '/wiki/$1', + ] ); + } + + protected function tearDown() { + global $wgHooks; + $wgHooks = $this->hooks; + parent::tearDown(); + } + + protected function getTestModules() { + return parent::getTestModules() + [ + 'TitleLibraryTests' => __DIR__ . '/TitleLibraryTests.lua', + ]; + } + + public function testAddsLinks() { + $engine = $this->getEngine(); + $interpreter = $engine->getInterpreter(); + + // Loading a title should create a link + $links = $engine->getParser()->getOutput()->getLinks(); + $this->assertFalse( isset( $links[NS_PROJECT]['Referenced_from_Lua'] ) ); + + $interpreter->callFunction( $interpreter->loadString( + 'local _ = mw.title.new( "Project:Referenced from Lua" ).id', 'reference title' + ) ); + + $links = $engine->getParser()->getOutput()->getLinks(); + $this->assertArrayHasKey( NS_PROJECT, $links ); + $this->assertArrayHasKey( 'Referenced_from_Lua', $links[NS_PROJECT] ); + + // Loading the page content should create a templatelink + $templates = $engine->getParser()->getOutput()->getTemplates(); + $this->assertFalse( isset( $links[NS_PROJECT]['Loaded_from_Lua'] ) ); + + $interpreter->callFunction( $interpreter->loadString( + 'mw.title.new( "Project:Loaded from Lua" ):getContent()', 'load title' + ) ); + + $templates = $engine->getParser()->getOutput()->getTemplates(); + $this->assertArrayHasKey( NS_PROJECT, $templates ); + $this->assertArrayHasKey( 'Loaded_from_Lua', $templates[NS_PROJECT] ); + } +} diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/TitleLibraryTests.lua b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/TitleLibraryTests.lua new file mode 100644 index 00000000..a5debe6b --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/TitleLibraryTests.lua @@ -0,0 +1,420 @@ +local testframework = require 'Module:TestFramework' + +local title, title_copy, title2, title3, title4, title5, title6u, title6s, title4p +if mw.title.testPageId then + title = mw.title.getCurrentTitle() + title_copy = mw.title.getCurrentTitle() + title2 = mw.title.new( 'Module:TestFramework' ) + title3 = mw.title.new( 'interwikiprefix:Module:TestFramework' ) + title4 = mw.title.new( 'Talk:Has/A/Subpage' ) + title5 = mw.title.new( 'Not/A/Subpage' ) + title4.fragment = 'frag' + + title4p = mw.title.new( 'Talk:Has/A' ) + + title6u = mw.title.new( 'Module_talk:Test_Framework' ) + title6u.fragment = '__frag__frag__' + + title6s = mw.title.new( 'Module talk:Test Framework' ) + title6s.fragment = ' frag frag ' +end + +local function prop_foreach( prop ) + return title[prop], title2[prop], title3[prop], title4[prop], title5[prop], title6u[prop], title6s[prop] +end + +local function func_foreach( func, ... ) + return title[func]( title, ... ), + title2[func]( title2, ... ), + title3[func]( title3, ... ), + title4[func]( title4, ... ), + title5[func]( title5, ... ), + title6u[func]( title6u, ... ), + title6s[func]( title6s, ... ) +end + +local function identity( ... ) + return ... +end + +local function test_space_normalization( s ) + local title = mw.title.new( s ) + return tostring( title ), tostring( title.fragment ) +end + +local function test_expensive_10() + for i = 1, 10 do + local _ = mw.title.new( tostring( i ) ).id + end + return 'did not error' +end + +local function test_expensive_11() + for i = 1, 11 do + local _ = mw.title.new( tostring( i ) ).id + end + return 'did not error' +end + +local function test_expensive_cached() + for i = 1, 100 do + local _ = mw.title.new( 'Title' ).id + end + return 'did not error' +end + +local function test_inexpensive() + for i = 1, 100 do + local _ = mw.title.new( 'Title' ).prefixedText + end + return 'did not error' +end + +local function test_getContent() + return mw.title.new( 'ScribuntoTestPage' ):getContent(), + mw.title.new( 'ScribuntoTestNonExistingPage' ):getContent() +end + +local function test_redirectTarget() + local targets = {} + local titles = { + 'ScribuntoTestRedirect', + 'ScribuntoTestNonRedirect', + 'ScribuntoTestNonExistingPage' + } + for _, title in ipairs( titles ) do + local target = mw.title.new( title ).redirectTarget + if title.prefixedText ~= nil then + target = title.prefixedText + end + table.insert( targets, target ) + end + return unpack( targets ) +end + +local function test_getCurrentTitle_fragment() + mw.title.getCurrentTitle().fragment = 'bad' + return mw.title.getCurrentTitle().fragment +end + +-- Tests +local tests = { + { name = 'tostring', func = identity, type = 'ToString', + args = { title, title2, title3, title4, title5, title6u, title6s }, + expect = { + 'Main Page', 'Module:TestFramework', 'interwikiprefix:Module:TestFramework', + 'Talk:Has/A/Subpage', 'Not/A/Subpage', + 'Module talk:Test Framework', 'Module talk:Test Framework' + } + }, + + { name = 'title.equal', func = mw.title.equals, + args = { title, title }, + expect = { true } + }, + { name = 'title.equal (2)', func = mw.title.equals, + args = { title, title_copy }, + expect = { true } + }, + { name = 'title.equal (3)', func = mw.title.equals, + args = { title, title2 }, + expect = { false } + }, + { name = '==', func = function () + return rawequal( title, title_copy ), title == title, title == title_copy, title == title2 + end, + expect = { false, true, true, false } + }, + + { name = 'title.compare', func = mw.title.compare, + args = { title, title }, + expect = { 0 } + }, + { name = 'title.compare (2)', func = mw.title.compare, + args = { title, title_copy }, + expect = { 0 } + }, + { name = 'title.compare (3)', func = mw.title.compare, + args = { title, title2 }, + expect = { -1 } + }, + { name = 'title.compare (4)', func = mw.title.compare, + args = { title2, title }, + expect = { 1 } + }, + { name = 'title.compare (5)', func = mw.title.compare, + args = { title2, title3 }, + expect = { -1 } + }, + { name = 'title.compare (6)', func = mw.title.compare, + args = { title6s, title6u }, + expect = { 0 } + }, + { name = '<', func = function () + return title < title, title < title_copy, title < title2, title2 < title + end, + expect = { false, false, true, false } + }, + { name = '<=', func = function () + return title <= title, title <= title_copy, title <= title2, title2 <= title + end, + expect = { true, true, true, false } + }, + + { name = 'title.new with namespace', func = mw.title.new, type = 'ToString', + args = { 'TestFramework', 'Module' }, + expect = { 'Module:TestFramework' } + }, + { name = 'title.new with namespace (2)', func = mw.title.new, type = 'ToString', + args = { 'TestFramework', mw.site.namespaces.Module.id }, + expect = { 'Module:TestFramework' } + }, + { name = 'title.new with namespace (3)', func = mw.title.new, type = 'ToString', + args = { 'Template:TestFramework', 'Module' }, + expect = { 'Template:TestFramework' } + }, + { name = 'title.new space normalization', func = test_space_normalization, + args = { ' __ Template __ : __ Test _ Framework __ # _ frag _ frag _ ' }, + expect = { 'Template:Test Framework', ' frag frag' } + }, + { name = 'title.new with invalid title', func = mw.title.new, + args = { '<bad title>' }, + expect = { nil } + }, + { name = 'title.new with nonexistent pageid', func = mw.title.new, + args = { -1 }, + expect = { nil } + }, + { name = 'title.new with pageid 0', func = mw.title.new, + args = { 0 }, + expect = { nil } + }, + { name = 'title.new with existing pageid', func = mw.title.new, type = 'ToString', + args = { mw.title.testPageId }, + expect = { 'ScribuntoTestPage' } + }, + + { name = 'title.makeTitle', func = mw.title.makeTitle, type = 'ToString', + args = { 'Module', 'TestFramework' }, + expect = { 'Module:TestFramework' } + }, + { name = 'title.makeTitle (2)', func = mw.title.makeTitle, type = 'ToString', + args = { mw.site.namespaces.Module.id, 'TestFramework' }, + expect = { 'Module:TestFramework' } + }, + { name = 'title.makeTitle (3)', func = mw.title.makeTitle, type = 'ToString', + args = { mw.site.namespaces.Module.id, 'Template:TestFramework' }, + expect = { 'Module:Template:TestFramework' } + }, + + { name = '.isLocal', func = prop_foreach, + args = { 'isLocal' }, + expect = { true, true, false, true, true, true, true } + }, + { name = '.isTalkPage', func = prop_foreach, + args = { 'isTalkPage' }, + expect = { false, false, false, true, false, true, true } + }, + { name = '.isSubpage', func = prop_foreach, + args = { 'isSubpage' }, + expect = { false, false, false, true, false, false, false } + }, + { name = '.text', func = prop_foreach, + args = { 'text' }, + expect = { + 'Main Page', 'TestFramework', 'Module:TestFramework', 'Has/A/Subpage', 'Not/A/Subpage', + 'Test Framework', 'Test Framework' + } + }, + { name = '.prefixedText', func = prop_foreach, + args = { 'prefixedText' }, + expect = { + 'Main Page', 'Module:TestFramework', 'interwikiprefix:Module:TestFramework', + 'Talk:Has/A/Subpage', 'Not/A/Subpage', 'Module talk:Test Framework', 'Module talk:Test Framework', + } + }, + { name = '.rootText', func = prop_foreach, + args = { 'rootText' }, + expect = { + 'Main Page', 'TestFramework', 'Module:TestFramework', 'Has', 'Not/A/Subpage', + 'Test Framework', 'Test Framework' + } + }, + { name = '.baseText', func = prop_foreach, + args = { 'baseText' }, + expect = { + 'Main Page', 'TestFramework', 'Module:TestFramework', 'Has/A', 'Not/A/Subpage', + 'Test Framework', 'Test Framework' + } + }, + { name = '.subpageText', func = prop_foreach, + args = { 'subpageText' }, + expect = { + 'Main Page', 'TestFramework', 'Module:TestFramework', 'Subpage', 'Not/A/Subpage', + 'Test Framework', 'Test Framework' + } + }, + { name = '.fullText', func = prop_foreach, + args = { 'fullText' }, + expect = { + 'Main Page', 'Module:TestFramework', 'interwikiprefix:Module:TestFramework', + 'Talk:Has/A/Subpage#frag', 'Not/A/Subpage', + 'Module talk:Test Framework# frag frag', 'Module talk:Test Framework# frag frag' + } + }, + { name = '.subjectNsText', func = prop_foreach, + args = { 'subjectNsText' }, + expect = { '', 'Module', '', '', '', 'Module', 'Module' } + }, + { name = '.fragment', func = prop_foreach, + args = { 'fragment' }, + expect = { '', '', '', 'frag', '', ' frag frag', ' frag frag' } + }, + { name = '.interwiki', func = prop_foreach, + args = { 'interwiki' }, + expect = { '', '', 'interwikiprefix', '', '', '', '' } + }, + { name = '.namespace', func = prop_foreach, + args = { 'namespace' }, + expect = { + 0, mw.site.namespaces.Module.id, 0, 1, 0, + mw.site.namespaces.Module_talk.id, mw.site.namespaces.Module_talk.id + } + }, + { name = '.protectionLevels', func = prop_foreach, + args = { 'protectionLevels' }, + expect = { + { edit = {}, move = {} }, { edit = { 'sysop', 'bogus' }, move = { 'sysop', 'bogus' } }, + {}, { create = { 'sysop' } }, { edit = { 'autoconfirmed' }, move = { 'sysop' } }, + { edit = {}, move = { 'sysop' } }, { edit = {}, move = { 'sysop' } } + } + }, + { name = '.cascadingProtection', func = prop_foreach, + args = { 'cascadingProtection' }, + expect = { + { restrictions = { edit = { 'sysop' } }, sources = { 'Lockbox', 'Lockbox2' } }, { restrictions = {}, sources = {} }, + { restrictions = {}, sources = {} }, { restrictions = {}, sources = {} }, { restrictions = {}, sources = {} }, + { restrictions = {}, sources = {} }, { restrictions = {}, sources = {} } + } + }, + { name = '.inNamespace()', func = func_foreach, + args = { 'inNamespace', 'Module' }, + expect = { false, true, false, false, false, false, false } + }, + { name = '.inNamespace() 2', func = func_foreach, + args = { 'inNamespace', mw.site.namespaces.Module.id }, + expect = { false, true, false, false, false, false, false } + }, + { name = '.inNamespaces()', func = func_foreach, + args = { 'inNamespaces', 0, 1 }, + expect = { true, false, true, true, true, false, false } + }, + { name = '.hasSubjectNamespace()', func = func_foreach, + args = { 'hasSubjectNamespace', 0 }, + expect = { true, false, true, true, true, false, false } + }, + { name = '.isSubpageOf() 1', func = func_foreach, + args = { 'isSubpageOf', title }, + expect = { false, false, false, false, false, false, false } + }, + { name = '.isSubpageOf() 2', func = func_foreach, + args = { 'isSubpageOf', title4p }, + expect = { false, false, false, true, false, false, false } + }, + { name = '.partialUrl()', func = func_foreach, + args = { 'partialUrl' }, + expect = { + 'Main_Page', 'TestFramework', 'Module:TestFramework', 'Has/A/Subpage', 'Not/A/Subpage', + 'Test_Framework', 'Test_Framework' + } + }, + { name = '.fullUrl()', func = func_foreach, + args = { 'fullUrl' }, + expect = { + '//wiki.local/wiki/Main_Page', + '//wiki.local/wiki/Module:TestFramework', + '//test.wikipedia.org/wiki/Module:TestFramework', + '//wiki.local/wiki/Talk:Has/A/Subpage#frag', + '//wiki.local/wiki/Not/A/Subpage', + '//wiki.local/wiki/Module_talk:Test_Framework#_frag_frag', + '//wiki.local/wiki/Module_talk:Test_Framework#_frag_frag', + } + }, + { name = '.fullUrl() 2', func = func_foreach, + args = { 'fullUrl', { action = 'test' } }, + expect = { + '//wiki.local/w/index.php?title=Main_Page&action=test', + '//wiki.local/w/index.php?title=Module:TestFramework&action=test', + '//test.wikipedia.org/wiki/Module:TestFramework?action=test', + '//wiki.local/w/index.php?title=Talk:Has/A/Subpage&action=test#frag', + '//wiki.local/w/index.php?title=Not/A/Subpage&action=test', + '//wiki.local/w/index.php?title=Module_talk:Test_Framework&action=test#_frag_frag', + '//wiki.local/w/index.php?title=Module_talk:Test_Framework&action=test#_frag_frag', + } + }, + { name = '.fullUrl() 3', func = func_foreach, + args = { 'fullUrl', nil, 'http' }, + expect = { + 'http://wiki.local/wiki/Main_Page', + 'http://wiki.local/wiki/Module:TestFramework', + 'http://test.wikipedia.org/wiki/Module:TestFramework', + 'http://wiki.local/wiki/Talk:Has/A/Subpage#frag', + 'http://wiki.local/wiki/Not/A/Subpage', + 'http://wiki.local/wiki/Module_talk:Test_Framework#_frag_frag', + 'http://wiki.local/wiki/Module_talk:Test_Framework#_frag_frag', + } + }, + { name = '.localUrl()', func = func_foreach, + args = { 'localUrl' }, + expect = { + '/wiki/Main_Page', + '/wiki/Module:TestFramework', + '//test.wikipedia.org/wiki/Module:TestFramework', + '/wiki/Talk:Has/A/Subpage', + '/wiki/Not/A/Subpage', + '/wiki/Module_talk:Test_Framework', + '/wiki/Module_talk:Test_Framework', + } + }, + { name = '.canonicalUrl()', func = func_foreach, + args = { 'canonicalUrl' }, + expect = { + 'http://wiki.local/wiki/Main_Page', + 'http://wiki.local/wiki/Module:TestFramework', + 'http://test.wikipedia.org/wiki/Module:TestFramework', + 'http://wiki.local/wiki/Talk:Has/A/Subpage#frag', + 'http://wiki.local/wiki/Not/A/Subpage', + 'http://wiki.local/wiki/Module_talk:Test_Framework#_frag_frag', + 'http://wiki.local/wiki/Module_talk:Test_Framework#_frag_frag', + } + }, + + { name = '.getContent()', func = test_getContent, + expect = { + '{{int:mainpage}}<includeonly>...</includeonly><noinclude>...</noinclude>', + nil, + } + }, + + { name = '.redirectTarget', func = test_redirectTarget, type = 'ToString', + expect = { 'ScribuntoTestTarget', false, false } + }, + + { name = 'not quite too many expensive functions', func = test_expensive_10, + expect = { 'did not error' } + }, + { name = 'too many expensive functions', func = test_expensive_11, + expect = 'too many expensive function calls' + }, + { name = "previously cached titles shouldn't count as expensive", func = test_expensive_cached, + expect = { 'did not error' } + }, + { name = "inexpensive actions shouldn't count as expensive", func = test_inexpensive, + expect = { 'did not error' } + }, + { name = "fragments don't leak via getCurrentTitle()", func = test_getCurrentTitle_fragment, + expect = { '' } + }, +} + +return testframework.getTestProvider( tests ) diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/UriLibraryTest.php b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/UriLibraryTest.php new file mode 100644 index 00000000..80137c84 --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/UriLibraryTest.php @@ -0,0 +1,26 @@ +<?php + +// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps +class Scribunto_LuaUriLibraryTest extends Scribunto_LuaEngineTestBase { + protected static $moduleName = 'UriLibraryTests'; + + protected function setUp() { + parent::setUp(); + + $this->setMwGlobals( [ + 'wgServer' => '//wiki.local', + 'wgCanonicalServer' => 'http://wiki.local', + 'wgUsePathInfo' => true, + 'wgActionPaths' => [], + 'wgScript' => '/w/index.php', + 'wgScriptPath' => '/w', + 'wgArticlePath' => '/wiki/$1', + ] ); + } + + protected function getTestModules() { + return parent::getTestModules() + [ + 'UriLibraryTests' => __DIR__ . '/UriLibraryTests.lua', + ]; + } +} diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/UriLibraryTests.lua b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/UriLibraryTests.lua new file mode 100644 index 00000000..7bf68c93 --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/UriLibraryTests.lua @@ -0,0 +1,182 @@ +local testframework = require 'Module:TestFramework' + +local function test_new( arg ) + -- Skip the functions + local ret = {} + for k, v in pairs( mw.uri.new( arg ) ) do + if type( v ) ~= 'function' then + ret[k] = v + end + end + return ret +end + +-- Tests +local tests = { + { name = 'uri.encode', func = mw.uri.encode, + args = { '__foo b\195\161r + baz__' }, + expect = { '__foo+b%C3%A1r+%2B+baz__' } + }, + { name = 'uri.encode QUERY', func = mw.uri.encode, + args = { '__foo b\195\161r + /baz/__', 'QUERY' }, + expect = { '__foo+b%C3%A1r+%2B+%2Fbaz%2F__' } + }, + { name = 'uri.encode PATH', func = mw.uri.encode, + args = { '__foo b\195\161r + /baz/__', 'PATH' }, + expect = { '__foo%20b%C3%A1r%20%2B%20%2Fbaz%2F__' } + }, + { name = 'uri.encode WIKI', func = mw.uri.encode, + args = { '__foo b\195\161r + /baz/__', 'WIKI' }, + expect = { '__foo_b%C3%A1r_%2B_/baz/__' } + }, + + { name = 'uri.decode', func = mw.uri.decode, + args = { '__foo+b%C3%A1r+%2B+baz__' }, + expect = { '__foo b\195\161r + baz__' } + }, + { name = 'uri.decode QUERY', func = mw.uri.decode, + args = { '__foo+b%C3%A1r+%2B+baz__', 'QUERY' }, + expect = { '__foo b\195\161r + baz__' } + }, + { name = 'uri.decode PATH', func = mw.uri.decode, + args = { '__foo+b%C3%A1r+%2B+baz__', 'PATH' }, + expect = { '__foo+b\195\161r+++baz__' } + }, + { name = 'uri.decode WIKI', func = mw.uri.decode, + args = { '__foo+b%C3%A1r+%2B+baz__', 'WIKI' }, + expect = { ' foo+b\195\161r+++baz ' } + }, + + { name = 'uri.anchorEncode', func = mw.uri.anchorEncode, + args = { '__foo b\195\161r__' }, + expect = { 'foo_b.C3.A1r' } + }, + + { name = 'uri.new', func = test_new, + args = { 'http://www.example.com/test?foo=1&bar&baz=1&baz=2#fragment' }, + expect = { + { + protocol = 'http', + host = 'www.example.com', + hostPort = 'www.example.com', + authority = 'www.example.com', + path = '/test', + query = { + foo = '1', + bar = false, + baz = { '1', '2' }, + }, + queryString = 'foo=1&bar&baz=1&baz=2', + fragment = 'fragment', + relativePath = '/test?foo=1&bar&baz=1&baz=2#fragment', + }, + }, + }, + + { name = 'uri.new', func = mw.uri.new, type = 'ToString', + args = { 'http://www.example.com/test?foo=1&bar&baz=1&baz=2#fragment' }, + expect = { 'http://www.example.com/test?foo=1&bar&baz=1&baz=2#fragment' }, + }, + + { name = 'uri.localUrl( Example )', func = mw.uri.localUrl, type = 'ToString', + args = { 'Example' }, + expect = { '/wiki/Example' }, + }, + { name = 'uri.localUrl( Example, string )', func = mw.uri.localUrl, type = 'ToString', + args = { 'Example', 'action=edit' }, + expect = { '/w/index.php?title=Example&action=edit' }, + }, + { name = 'uri.localUrl( Example, table )', func = mw.uri.localUrl, type = 'ToString', + args = { 'Example', { action = 'edit' } }, + expect = { '/w/index.php?title=Example&action=edit' }, + }, + + { name = 'uri.fullUrl( Example )', func = mw.uri.fullUrl, type = 'ToString', + args = { 'Example' }, + expect = { '//wiki.local/wiki/Example' }, + }, + { name = 'uri.fullUrl( Example, string )', func = mw.uri.fullUrl, type = 'ToString', + args = { 'Example', 'action=edit' }, + expect = { '//wiki.local/w/index.php?title=Example&action=edit' }, + }, + { name = 'uri.fullUrl( Example, table )', func = mw.uri.fullUrl, type = 'ToString', + args = { 'Example', { action = 'edit' } }, + expect = { '//wiki.local/w/index.php?title=Example&action=edit' }, + }, + + { name = 'uri.canonicalUrl( Example )', func = mw.uri.canonicalUrl, type = 'ToString', + args = { 'Example' }, + expect = { 'http://wiki.local/wiki/Example' }, + }, + { name = 'uri.canonicalUrl( Example, string )', func = mw.uri.canonicalUrl, type = 'ToString', + args = { 'Example', 'action=edit' }, + expect = { 'http://wiki.local/w/index.php?title=Example&action=edit' }, + }, + { name = 'uri.canonicalUrl( Example, table )', func = mw.uri.canonicalUrl, type = 'ToString', + args = { 'Example', { action = 'edit' } }, + expect = { 'http://wiki.local/w/index.php?title=Example&action=edit' }, + }, + + { name = 'uri.new with empty query string', func = mw.uri.new, type = 'ToString', + args = { 'http://wiki.local/w/index.php?' }, + expect = { 'http://wiki.local/w/index.php?' }, + }, + + { name = 'uri.new with empty fragment', func = mw.uri.new, type = 'ToString', + args = { 'http://wiki.local/w/index.php#' }, + expect = { 'http://wiki.local/w/index.php#' }, + }, +} + +-- Add tests to test round-tripping for every combination of parameters +local bits = { [0] = false, false, false, false, false, false, false, false, false } +local ct = 0 +while not bits[8] do + local url = {} + if bits[0] then + url[#url+1] = 'http:' + end + if bits[1] or bits[2] or bits[3] or bits[4] then + url[#url+1] = '//' + end + if bits[1] then + url[#url+1] = 'user' + end + if bits[2] then + url[#url+1] = ':password' + end + if bits[1] or bits[2] then + url[#url+1] = '@' + end + if bits[3] then + url[#url+1] = 'host.example.com' + end + if bits[4] then + url[#url+1] = ':123' + end + if bits[5] then + url[#url+1] = '/path' + end + if bits[6] then + url[#url+1] = '?query=1' + end + if bits[7] then + url[#url+1] = '#fragment' + end + + url = table.concat( url, '' ) + tests[#tests+1] = { name = 'uri.new (' .. ct .. ')', func = mw.uri.new, type = 'ToString', + args = { url }, + expect = { url }, + } + ct = ct + 1 + + for i = 0, 8 do + bits[i] = not bits[i] + if bits[i] then + break + end + end +end + +return testframework.getTestProvider( tests ) diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/UstringLibraryNormalizationTests.lua b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/UstringLibraryNormalizationTests.lua new file mode 100644 index 00000000..97d794a1 --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/UstringLibraryNormalizationTests.lua @@ -0,0 +1,33 @@ +local function tohex( s ) + local t = { s } + for c in mw.ustring.gcodepoint( s ) do + t[#t + 1] = string.format( "%x", c ) + end + return table.concat( t, '\t' ) +end + +return { + run = function ( c1, c2, c3, c4, c5 ) + return + tohex( mw.ustring.toNFC( c1 ) ), + tohex( mw.ustring.toNFC( c2 ) ), + tohex( mw.ustring.toNFC( c3 ) ), + tohex( mw.ustring.toNFC( c4 ) ), + tohex( mw.ustring.toNFC( c5 ) ), + tohex( mw.ustring.toNFD( c1 ) ), + tohex( mw.ustring.toNFD( c2 ) ), + tohex( mw.ustring.toNFD( c3 ) ), + tohex( mw.ustring.toNFD( c4 ) ), + tohex( mw.ustring.toNFD( c5 ) ), + tohex( mw.ustring.toNFKC( c1 ) ), + tohex( mw.ustring.toNFKC( c2 ) ), + tohex( mw.ustring.toNFKC( c3 ) ), + tohex( mw.ustring.toNFKC( c4 ) ), + tohex( mw.ustring.toNFKC( c5 ) ), + tohex( mw.ustring.toNFKD( c1 ) ), + tohex( mw.ustring.toNFKD( c2 ) ), + tohex( mw.ustring.toNFKD( c3 ) ), + tohex( mw.ustring.toNFKD( c4 ) ), + tohex( mw.ustring.toNFKD( c5 ) ) + end +} diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/UstringLibraryPureLuaTest.php b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/UstringLibraryPureLuaTest.php new file mode 100644 index 00000000..9743e0d5 --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/UstringLibraryPureLuaTest.php @@ -0,0 +1,35 @@ +<?php + +require_once __DIR__ . '/UstringLibraryTest.php'; + +// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps +class Scribunto_LuaUstringLibraryPureLuaTest extends Scribunto_LuaUstringLibraryTest { + protected function setUp() { + parent::setUp(); + + // Override mw.ustring with the pure-Lua version + $interpreter = $this->getEngine()->getInterpreter(); + $interpreter->callFunction( + $interpreter->loadString( ' + local ustring = require( "ustring" ) + ustring.maxStringLength = mw.ustring.maxStringLength + ustring.maxPatternLength = mw.ustring.maxPatternLength + mw.ustring = ustring + ', 'fortest' ) + ); + } + + /** + * @dataProvider providePCREErrors + */ + public function testPCREErrors( $ini, $args, $error ) { + // Not applicable + $this->assertTrue( true ); + } + + public static function providePCREErrors() { + return [ + [ [], [], null ], + ]; + } +} diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/UstringLibraryTest.php b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/UstringLibraryTest.php new file mode 100644 index 00000000..61120067 --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/UstringLibraryTest.php @@ -0,0 +1,192 @@ +<?php + +use Wikimedia\ScopedCallback; + +// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps +class Scribunto_LuaUstringLibraryTest extends Scribunto_LuaEngineTestBase { + protected static $moduleName = 'UstringLibraryTests'; + + private $normalizationDataProvider = null; + + protected function tearDown() { + if ( $this->normalizationDataProvider ) { + $this->normalizationDataProvider->destroy(); + $this->normalizationDataProvider = null; + } + parent::tearDown(); + } + + protected function getTestModules() { + return parent::getTestModules() + [ + 'UstringLibraryTests' => __DIR__ . '/UstringLibraryTests.lua', + 'UstringLibraryNormalizationTests' => __DIR__ . '/UstringLibraryNormalizationTests.lua', + ]; + } + + public function testUstringLibraryNormalizationTestsAvailable() { + if ( UstringLibraryNormalizationTestProvider::available( $err ) ) { + $this->assertTrue( true ); + } else { + $this->markTestSkipped( $err ); + } + } + + public function provideUstringLibraryNormalizationTests() { + if ( !$this->normalizationDataProvider ) { + $this->normalizationDataProvider = + new UstringLibraryNormalizationTestProvider( $this->getEngine() ); + } + return $this->normalizationDataProvider; + } + + /** + * @dataProvider provideUstringLibraryNormalizationTests + */ + public function testUstringLibraryNormalizationTests( $name, $c1, $c2, $c3, $c4, $c5 ) { + $this->luaTestName = "UstringLibraryNormalization: $name"; + $dataProvider = $this->provideUstringLibraryNormalizationTests(); + $expected = [ + $c2, $c2, $c2, $c4, $c4, // NFC + $c3, $c3, $c3, $c5, $c5, // NFD + $c4, $c4, $c4, $c4, $c4, // NFKC + $c5, $c5, $c5, $c5, $c5, // NFKD + ]; + foreach ( $expected as &$e ) { + $chars = array_values( unpack( 'N*', mb_convert_encoding( $e, 'UTF-32BE', 'UTF-8' ) ) ); + foreach ( $chars as &$c ) { + $c = sprintf( "%x", $c ); + } + $e = "$e\t" . implode( "\t", $chars ); + } + $actual = $dataProvider->runNorm( $c1, $c2, $c3, $c4, $c5 ); + $this->assertSame( $expected, $actual ); + $this->luaTestName = null; + } + + /** + * @dataProvider providePCREErrors + */ + public function testPCREErrors( $ini, $args, $error ) { + $reset = []; + foreach ( $ini as $key => $value ) { + $old = ini_set( $key, $value ); + if ( $old === false ) { + $this->markTestSkipped( "Failed to set ini setting $key = $value" ); + } + $reset[] = new ScopedCallback( 'ini_set', [ $key, $old ] ); + } + + $interpreter = $this->getEngine()->getInterpreter(); + $func = $interpreter->loadString( 'return mw.ustring.gsub( ... )', 'fortest' ); + try { + call_user_func_array( + [ $interpreter, 'callFunction' ], + array_merge( [ $func ], $args ) + ); + $this->fail( 'Expected exception not thrown' ); + } catch ( Scribunto_LuaError $e ) { + $this->assertSame( $error, $e->getMessage() ); + } + } + + public static function providePCREErrors() { + return [ + [ + [ 'pcre.backtrack_limit' => 10 ], + [ 'zzzzzzzzzzzzzzzzzzzz', '^(.-)[abc]*$', '%1' ], + 'Lua error: PCRE backtrack limit reached while matching pattern \'^(.-)[abc]*$\'.' + ], + // @TODO: Figure out patterns that hit other PCRE limits + ]; + } +} + +class UstringLibraryNormalizationTestProvider extends Scribunto_LuaDataProvider { + protected $file = null; + protected $current = null; + protected static $static = [ + '1E0A 0323;1E0C 0307;0044 0323 0307;1E0C 0307;0044 0323 0307;', + false + ]; + + public static function available( &$message = null ) { + if ( is_readable( __DIR__ . '/NormalizationTest.txt' ) ) { + return true; + } + $message = wordwrap( 'Download the Unicode Normalization Test Suite from ' . + 'http://unicode.org/Public/6.0.0/ucd/NormalizationTest.txt and save as ' . + __DIR__ . '/NormalizationTest.txt to run normalization tests. Note that ' . + 'running these tests takes quite some time.' ); + return false; + } + + public function __construct( $engine ) { + parent::__construct( $engine, 'UstringLibraryNormalizationTests' ); + if ( self::available() ) { + $this->file = fopen( __DIR__ . '/NormalizationTest.txt', 'r' ); + } + $this->rewind(); + } + + public function destory() { + if ( $this->file ) { + fclose( $this->file ); + $this->file = null; + } + parent::destory(); + } + + public function rewind() { + if ( $this->file ) { + rewind( $this->file ); + } + $this->key = 0; + $this->next(); + } + + public function valid() { + if ( $this->file ) { + $v = !feof( $this->file ); + } else { + $v = $this->key < count( self::$static ); + } + return $v; + } + + public function current() { + return $this->current; + } + + public function next() { + $this->current = [ null, null, null, null, null, null ]; + while ( $this->valid() ) { + if ( $this->file ) { + $line = fgets( $this->file ); + } else { + $line = self::$static[$this->key]; + } + $this->key++; + if ( preg_match( '/^((?:[0-9A-F ]+;){5})/', $line, $m ) ) { + $line = rtrim( $m[1], ';' ); + $ret = [ $line ]; + foreach ( explode( ';', $line ) as $field ) { + $args = [ 'N*' ]; + foreach ( explode( ' ', $field ) as $char ) { + $args[] = hexdec( $char ); + } + $s = call_user_func_array( 'pack', $args ); + $s = mb_convert_encoding( $s, 'UTF-8', 'UTF-32BE' ); + $ret[] = $s; + } + $this->current = $ret; + return; + } + } + } + + public function runNorm( $c1, $c2, $c3, $c4, $c5 ) { + return $this->engine->getInterpreter()->callFunction( $this->exports['run'], + $c1, $c2, $c3, $c4, $c5 + ); + } +} diff --git a/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/UstringLibraryTests.lua b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/UstringLibraryTests.lua new file mode 100644 index 00000000..2b880ce1 --- /dev/null +++ b/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/UstringLibraryTests.lua @@ -0,0 +1,730 @@ +local testframework = require 'Module:TestFramework' + +local str1 = "\0\127\194\128\223\191\224\160\128\239\191\191\240\144\128\128\244\143\191\191" +local str2 = "foo bar főó foó baz foooo foofoo fo" +local str3 = "??? foo bar főó foó baz foooo foofoo fo ok?" +local str4 = {} +for i = 1, 10000/4 do + str4[i] = "főó " +end +str4 = table.concat( str4 ) + +local function testLongGcodepoint() + local ret = {} + local i = 1 + for cp in mw.ustring.gcodepoint( str4 ) do + if i <= 4 or i > 9996 then + ret[i] = cp + end + i = i + 1 + end + return ret +end + +return testframework.getTestProvider( { + { name = 'isutf8: valid string', func = mw.ustring.isutf8, + args = { "\0 \127 \194\128 \223\191 \224\160\128 \239\191\191 \240\144\128\128 \244\143\191\191" }, + expect = { true } + }, + { name = 'isutf8: out of range character', func = mw.ustring.isutf8, + args = { "\244\144\128\128" }, + expect = { false } + }, + { name = 'isutf8: insufficient continuation bytes', func = mw.ustring.isutf8, + args = { "\240\128\128" }, + expect = { false } + }, + { name = 'isutf8: excess continuation bytes', func = mw.ustring.isutf8, + args = { "\194\128\128" }, + expect = { false } + }, + { name = 'isutf8: bare continuation byte', func = mw.ustring.isutf8, + args = { "\128" }, + expect = { false } + }, + { name = 'isutf8: overlong encoding', func = mw.ustring.isutf8, + args = { "\192\128" }, + expect = { false } + }, + { name = 'isutf8: overlong encoding (2)', func = mw.ustring.isutf8, + args = { "\193\191" }, + expect = { false } + }, + + { name = 'byteoffset: (1)', func = mw.ustring.byteoffset, + args = { "fóo", 1 }, + expect = { 1 } + }, + { name = 'byteoffset: (2)', func = mw.ustring.byteoffset, + args = { "fóo", 2 }, + expect = { 2 } + }, + { name = 'byteoffset: (3)', func = mw.ustring.byteoffset, + args = { "fóo", 3 }, + expect = { 4 } + }, + { name = 'byteoffset: (4)', func = mw.ustring.byteoffset, + args = { "fóo", 4 }, + expect = { nil } + }, + { name = 'byteoffset: (0,1)', func = mw.ustring.byteoffset, + args = { "fóo", 0, 1 }, + expect = { 1 } + }, + { name = 'byteoffset: (0,2)', func = mw.ustring.byteoffset, + args = { "fóo", 0, 2 }, + expect = { 2 } + }, + { name = 'byteoffset: (0,3)', func = mw.ustring.byteoffset, + args = { "fóo", 0, 3 }, + expect = { 2 } + }, + { name = 'byteoffset: (0,4)', func = mw.ustring.byteoffset, + args = { "fóo", 0, 4 }, + expect = { 4 } + }, + { name = 'byteoffset: (0,5)', func = mw.ustring.byteoffset, + args = { "fóo", 0, 5 }, + expect = { nil } + }, + { name = 'byteoffset: (0,-1)', func = mw.ustring.byteoffset, + args = { "fóo", 0, -1 }, + expect = { 4 } + }, + { name = 'byteoffset: (0,-1)', func = mw.ustring.byteoffset, + args = { "foó", 0, -1 }, + expect = { 3 } + }, + { name = 'byteoffset: (1,-1)', func = mw.ustring.byteoffset, + args = { "fóo", 1, -1 }, + expect = { 4 } + }, + { name = 'byteoffset: (1,-1)', func = mw.ustring.byteoffset, + args = { "foó", 1, -1 }, + expect = { nil } + }, + + { name = 'codepoint: whole string', func = mw.ustring.codepoint, + args = { str1, 1, -1 }, + expect = { 0, 0x7f, 0x80, 0x7ff, 0x800, 0xffff, 0x10000, 0x10ffff } + }, + { name = 'codepoint: substring', func = mw.ustring.codepoint, + args = { str1, 5, -2 }, + expect = { 0x800, 0xffff, 0x10000 } + }, + { name = 'codepoint: (5,4)', func = mw.ustring.codepoint, + args = { str1, 5, 4 }, + expect = {} + }, + { name = 'codepoint: (1,0)', func = mw.ustring.codepoint, + args = { str1, 1, 0 }, + expect = {} + }, + { name = 'codepoint: (9,9)', func = mw.ustring.codepoint, + args = { str1, 9, 9 }, + expect = {} + }, + { name = 'codepoint: end of a really long string', func = mw.ustring.codepoint, + args = { str4, 9000, 9004 }, + expect = { 0x20, 0x66, 0x151, 0xf3, 0x20 } + }, + + { name = 'char: basic test', func = mw.ustring.char, + args = { 0, 0x7f, 0x80, 0x7ff, 0x800, 0xffff, 0x10000, 0x10ffff }, + expect = { str1 } + }, + { name = 'char: invalid codepoint', func = mw.ustring.char, + args = { 0x110000 }, + expect = "bad argument #1 to 'char' (value out of range)" + }, + { name = 'char: invalid value', func = mw.ustring.char, + args = { 'foo' }, + expect = "bad argument #1 to 'char' (number expected, got string)" + }, + + { name = 'len: basic test', func = mw.ustring.len, + args = { str1 }, + expect = { 8 } + }, + { name = 'len: invalid string', func = mw.ustring.len, + args = { "\244\144\128\128" }, + expect = { nil } + }, + + { name = 'sub: (4)', func = mw.ustring.sub, + args = { str1, 4 }, + expect = { "\223\191\224\160\128\239\191\191\240\144\128\128\244\143\191\191" } + }, + { name = 'sub: (4,7)', func = mw.ustring.sub, + args = { str1, 4, 7 }, + expect = { "\223\191\224\160\128\239\191\191\240\144\128\128" } + }, + { name = 'sub: (4,-1)', func = mw.ustring.sub, + args = { str1, 4, -1 }, + expect = { "\223\191\224\160\128\239\191\191\240\144\128\128\244\143\191\191" } + }, + { name = 'sub: (4,-2)', func = mw.ustring.sub, + args = { str1, 4, -2 }, + expect = { "\223\191\224\160\128\239\191\191\240\144\128\128" } + }, + { name = 'sub: (-2)', func = mw.ustring.sub, + args = { str1, -2 }, + expect = { "\240\144\128\128\244\143\191\191" } + }, + { name = 'sub: (9)', func = mw.ustring.sub, + args = { str1, 9 }, + expect = { "" } + }, + { name = 'sub: (0)', func = mw.ustring.sub, + args = { str1, 0 }, + expect = { str1 } + }, + { name = 'sub: (4,3)', func = mw.ustring.sub, + args = { str1, 4, 3 }, + expect = { "" } + }, + { name = 'sub: (1,0)', func = mw.ustring.sub, + args = { str2, 1, 0 }, + expect = { "" } + }, + { name = 'sub: (5,5)', func = mw.ustring.sub, + args = { str1, 5, 5 }, + expect = { "\224\160\128" } + }, + { name = 'sub: (9,9)', func = mw.ustring.sub, + args = { str1, 9, 9 }, + expect = { "" } + }, + { name = 'sub: empty string', func = mw.ustring.sub, + args = { '', 5 }, + expect = { "" } + }, + + { name = 'upper: basic test', func = mw.ustring.upper, + args = { "fóó?" }, + expect = { "FÓÓ?" } + }, + { name = 'lower: basic test', func = mw.ustring.lower, + args = { "FÓÓ?" }, + expect = { "fóó?" } + }, + + { name = 'find: (simple)', func = mw.ustring.find, + args = { "bar ¡foo bar", '¡foo' }, + expect = { 5, 8 } + }, + { name = 'find: (%)', func = mw.ustring.find, + args = { "bar ¡foo bar", '¡fo%+' }, + expect = { } + }, + { name = 'find: (%)', func = mw.ustring.find, + args = { "bar ¡fo+ bar", '¡fo%+' }, + expect = { 5, 8 } + }, + { name = 'find: (+)', func = mw.ustring.find, + args = { "bar ¡foo bar", '¡fo+' }, + expect = { 5, 8 } + }, + { name = 'find: (+) (2)', func = mw.ustring.find, + args = { "bar ¡foo bar", '¡fx+o+' }, + expect = {} + }, + { name = 'find: (?)', func = mw.ustring.find, + args = { "bar ¡foox bar", '¡foox?' }, + expect = { 5, 9 } + }, + { name = 'find: (?) (2)', func = mw.ustring.find, + args = { "bar ¡foo bar", '¡foox?' }, + expect = { 5, 8 } + }, + { name = 'find: (*)', func = mw.ustring.find, + args = { "bar ¡foo bar", '¡fx*oo' }, + expect = { 5, 8 } + }, + { name = 'find: (-)', func = mw.ustring.find, + args = { "bar ¡foo bar", '¡fo-' }, + expect = { 5, 6 } + }, + { name = 'find: (-)', func = mw.ustring.find, + args = { "bar ¡foo bar", '¡fo-o' }, + expect = { 5, 7 } + }, + { name = 'find: (-)', func = mw.ustring.find, + args = { "bar ¡foox bar", '¡fo-x' }, + expect = { 5, 9 } + }, + { name = 'find: (%a)', func = mw.ustring.find, + args = { "bar ¡foo bar", '¡f%a' }, + expect = { 5, 7 } + }, + { name = 'find: (%a, utf8)', func = mw.ustring.find, + args = { "bar ¡fóó bar", '¡f%a' }, + expect = { 5, 7 } + }, + { name = 'find: (%a, utf8 2)', func = mw.ustring.find, + args = { "bar ¡fóó bar", 'f%a' }, + expect = { 6, 7 } + }, + { name = 'find: (%a+)', func = mw.ustring.find, + args = { "bar ¡fóó bar", '¡f%a+' }, + expect = { 5, 8 } + }, + { name = 'find: ([]+)', func = mw.ustring.find, + args = { "bar ¡fóo bar", '¡f[oó]+' }, + expect = { 5, 8 } + }, + { name = 'find: ([-]+)', func = mw.ustring.find, + args = { "bar ¡fóo bar", '¡f[a-uá-ú]+' }, + expect = { 5, 8 } + }, + { name = 'find: ([-]+ 2)', func = mw.ustring.find, + args = { "bar ¡fóo bar", '¡f[a-ú]+' }, + expect = { 5, 8 } + }, + { name = 'find: (%b)', func = mw.ustring.find, + args = { "bar ¡<foo <foo> foo> bar", '¡%b<>' }, + expect = { 5, 20 } + }, + { name = 'find: (%b 2)', func = mw.ustring.find, + args = { "bar ¡(foo (foo) foo) bar", '¡%b()' }, + expect = { 5, 20 } + }, + { name = 'find: (%b 3)', func = mw.ustring.find, + args = { "bar ¡-foo-foo- bar", '¡%b--' }, + expect = { 5, 10 } + }, + { name = 'find: (%b 4)', func = mw.ustring.find, + args = { "bar «foo «foo» foo» bar", '%b«»' }, + expect = { 5, 19 } + }, + { name = 'find: (%b 5)', func = mw.ustring.find, + args = { "bar !foo !foo¡ foo¡ bar", '%b!¡' }, + expect = { 5, 19 } + }, + { name = 'find: (%b 6)', func = mw.ustring.find, + args = { "bar ¡foo ¡foo! foo! bar", '%b¡!' }, + expect = { 5, 19 } + }, + { name = 'find: (%b 7)', func = mw.ustring.find, + args = { "bar ¡foo¡foo¡ bar", '%b¡¡' }, + expect = { 5, 9 } + }, + { name = 'find: (%f)', func = mw.ustring.find, + args = { "foo ¡foobar ¡foo bar baz", '¡.-%f[%s]' }, + expect = { 5, 11 } + }, + { name = 'find: (%f 2)', func = mw.ustring.find, + args = { "foo ¡foobar ¡foo bar baz", '¡foo%f[%s]' }, + expect = { 13, 16 } + }, + { name = 'find: (%f 3)', func = mw.ustring.find, + args = { "foo foo¡foobar ¡foo bar baz", '%f[%S]¡.-%f[%s]' }, + expect = { 16, 19 } + }, + { name = 'find: (%f 4)', func = mw.ustring.find, + args = { "foo foo¡foobar ¡foo bar baz", '%f[%S]¡.-%f[%s]', 16 }, + expect = { 16, 19 } + }, + { name = 'find: (%f 5)', func = mw.ustring.find, + args = { "foo ¡bar baz", '%f[%Z]' }, + expect = { 1, 0 } + }, + { name = 'find: (%f 6)', func = mw.ustring.find, + args = { "foo ¡bar baz", '%f[%z]' }, + expect = { 13, 12 } + }, + { name = 'find: (%f 7)', func = mw.ustring.find, + args = { "foo ¡b\0r baz", '%f[%Z]', 2 }, + expect = { 8, 7 } + }, + { name = 'find: (%f 8)', func = mw.ustring.find, + args = { "\0foo ¡b\0r baz", '%f[%z]' }, + expect = { 8, 7 } + }, + { name = 'find: (%f 9)', func = mw.ustring.find, + args = { "\0foo ¡b\0r baz", '%f[%Z]' }, + expect = { 2, 1 } + }, + { name = 'find: (%A)', func = mw.ustring.find, + args = { "fóó? bar", '%A+' }, + expect = { 4, 5 } + }, + { name = 'find: (%W)', func = mw.ustring.find, + args = { "fóó? bar", '%W+' }, + expect = { 4, 5 } + }, + { name = 'find: ([^])', func = mw.ustring.find, + args = { "fóó? bar", '[^a-zó]+' }, + expect = { 4, 5 } + }, + { name = 'find: ([^] 2)', func = mw.ustring.find, + args = { "fó0? bar", '[^%a0-9]+' }, + expect = { 4, 5 } + }, + { name = 'find: ([^] 3)', func = mw.ustring.find, + args = { "¡fó0% bar", '¡[^%%]+' }, + expect = { 1, 4 } + }, + { name = 'find: ($)', func = mw.ustring.find, + args = { "¡foo1 ¡foo2 ¡foo3", '¡foo[0-9]+$' }, + expect = { 13, 17 } + }, + { name = 'find: (.*)', func = mw.ustring.find, + args = { "¡foo¡ ¡bar¡ baz", '¡.*¡' }, + expect = { 1, 11 } + }, + { name = 'find: (.-)', func = mw.ustring.find, + args = { "¡foo¡ ¡bar¡ baz", '¡.-¡' }, + expect = { 1, 5 } + }, + { name = 'find: plain', func = mw.ustring.find, + args = { "¡a¡ ¡.¡", '¡.¡', 1, true }, + expect = { 5, 7 } + }, + { name = 'find: empty delimiter', func = mw.ustring.find, + args = { "¡a¡ ¡.¡", '' }, + expect = { 1, 0 } + }, + { name = 'find: empty delimiter (2)', func = mw.ustring.find, + args = { "¡a¡ ¡.¡", '', 2 }, + expect = { 2, 1 } + }, + { name = 'find: plain + empty delimiter', func = mw.ustring.find, + args = { "¡a¡ ¡.¡", '', 1, true }, + expect = { 1, 0 } + }, + { name = 'find: plain + empty delimiter (2)', func = mw.ustring.find, + args = { "¡a¡ ¡.¡", '', 2, true }, + expect = { 2, 1 } + }, + { name = 'find: excessive init', func = mw.ustring.find, + args = { "¡a¡ ¡.¡", '()', 20 }, + expect = { 8, 7, 8 } + }, + { name = 'find: excessive init (2)', func = mw.ustring.find, + args = { "¡a¡ ¡.¡", '()', -20 }, + expect = { 1, 0, 1 } + }, + { name = 'find: plain + excessive init', func = mw.ustring.find, + args = { "¡a¡ ¡.¡", '', 20, true }, + expect = { 8, 7 } + }, + { name = 'find: plain + excessive init', func = mw.ustring.find, + args = { "¡a¡ ¡.¡", '', -20, true }, + expect = { 1, 0 } + }, + + { name = 'find: capture (1)', func = mw.ustring.find, + args = { "bar ¡foo bar", '(¡foo)' }, + expect = { 5, 8, '¡foo' } + }, + { name = 'find: capture (2)', func = mw.ustring.find, + args = { "bar ¡fóo bar", '(¡f%a+)' }, + expect = { 5, 8, '¡fóo' } + }, + { name = 'find: capture (3)', func = mw.ustring.find, + args = { "bar ¡fóo bar", '(¡f(%a)%a)' }, + expect = { 5, 8, '¡fóo', 'ó' } + }, + { name = 'find: capture (4)', func = mw.ustring.find, + args = { "bar ¡fóo bar", '(¡f(%a-)%a)' }, + expect = { 5, 7, '¡fó', '' } + }, + { name = 'find: capture (5)', func = mw.ustring.find, + args = { "bar ¡fóo bar", '()(()¡f()(%a)()%a())()' }, + expect = { 5, 8, 5, '¡fóo', 5, 7, 'ó', 8, 9, 9 } + }, + { name = 'find: capture (6)', func = mw.ustring.find, + args = { "fóó", "()(f)()(óó)()" }, + expect = { 1, 3, 1, 'f', 2, 'óó', 4 } + }, + { name = 'find: capture (7)', func = mw.ustring.find, + args = { "fóó fóó", "()(f)()(óó)()", 2 }, + expect = { 5, 7, 5, 'f', 6, 'óó', 8 } + }, + { name = 'find: (%1)', func = mw.ustring.find, + args = { "foo foofóó foófoó bar", '(f%a+)%1' }, + expect = { 12, 17, 'foó' } + }, + { name = 'find: deceptively-simple pattern', func = mw.ustring.find, + args = { "fóó", '([^a-z])' }, + expect = { 2, 2, 'ó' } + }, + { name = 'find: Bracket at start of a character set doesn\'t close', func = mw.ustring.find, + args = { "fóó", '()[]' }, + expect = "Missing close-bracket for character set beginning at pattern character 3" + }, + { name = 'find: Bracket at start of a negated character set doesn\'t close', func = mw.ustring.find, + args = { "fóó", '()[^]' }, + expect = "Missing close-bracket for character set beginning at pattern character 3" + }, + { name = 'find: Bracket at start of a character set is literal', func = mw.ustring.find, + args = { "foo]bar¿", '()([]])' }, + expect = { 4, 4, 4, ']' } + }, + { name = 'find: Bracket at start of a negated character set is literal', func = mw.ustring.find, + args = { "]bar¿", '()([^]])' }, + expect = { 2, 2, 2, 'b' } + }, + { name = 'find: Bracket at start of a character set can be a range endpoint', func = mw.ustring.find, + args = { "foo]bar¿", '()([]-z]+)' }, + expect = { 1, 7, 1, 'foo]bar' } + }, + { name = 'find: Bracket at start of a negated character can be a range endpoint', func = mw.ustring.find, + args = { "fOO]bar¿", '()([^]-z]+)' }, + expect = { 2, 3, 2, 'OO' } + }, + { name = 'find: Weird edge-case that was failing (1)', func = mw.ustring.find, + args = { "foo]ba-]r¿", '()([a]-%]+)' }, + expect = { 4, 4, 4, ']' } + }, + { name = 'find: Weird edge-case that was failing (2)', func = mw.ustring.find, + args = { "foo¿", '()[!-%]' }, + expect = "Missing close-bracket for character set beginning at pattern character 3" + }, + { name = 'find: Inverted range (1)', func = mw.ustring.find, + args = { "foo¿", '()([z-a]+)' }, + expect = { nil } + }, + { name = 'find: Inverted range (2)', func = mw.ustring.find, + args = { "foo¿", '()([^z-a]+)' }, + expect = { 1, 4, 1, 'foo¿' } + }, + { name = 'find: Inverted range (3)', func = mw.ustring.find, + args = { "foo¿", '()(f[z-a]o)' }, + expect = { nil } + }, + { name = 'find: Inverted range (4)', func = mw.ustring.find, + args = { "foo¿", '()(f[z-a]*o)' }, + expect = { 1, 2, 1, 'fo' } + }, + + { name = 'match: (1)', func = mw.ustring.match, + args = { "bar fóo bar", 'f%a+' }, + expect = { 'fóo' } + }, + { name = 'match: (2)', func = mw.ustring.match, + args = { "bar fóo bar", 'f(%a+)' }, + expect = { 'óo' } + }, + { name = 'match: empty pattern', func = mw.ustring.match, + args = { "¡a¡ ¡.¡", '()' }, + expect = { 1 } + }, + { name = 'match: empty pattern (2)', func = mw.ustring.match, + args = { "¡a¡ ¡.¡", '()', 2 }, + expect = { 2 } + }, + { name = 'match: excessive init', func = mw.ustring.match, + args = { "¡a¡ ¡.¡", '()', 20 }, + expect = { 8 } + }, + { name = 'match: excessive init (2)', func = mw.ustring.match, + args = { "¡a¡ ¡.¡", '()', -20 }, + expect = { 1 } + }, + + { name = 'gsub: (emtpy string, empty pattern)', func = mw.ustring.gsub, + args = { '', '', 'X' }, + expect = { 'X', 1 } + }, + { name = 'gsub: (emtpy string, one char pattern)', func = mw.ustring.gsub, + args = { '', 'á', 'X' }, + expect = { '', 0 } + }, + { name = 'gsub: (one char string, one char pattern)', func = mw.ustring.gsub, + args = { 'á', 'á', 'X' }, + expect = { 'X', 1 } + }, + { name = 'gsub: (one char string, empty pattern)', func = mw.ustring.gsub, + args = { 'á', '', 'X' }, + expect = { 'XáX', 2 } + }, + { name = 'gsub: (empty pattern with position captures)', func = mw.ustring.gsub, + args = { 'ábć', '()', '%1' }, + expect = { '1á2b3ć4', 4 } + }, + { name = 'gsub: (limited to 1 replacement)', func = mw.ustring.gsub, + args = { 'áá', 'á', 'X', 1 }, + expect = { 'Xá', 1 } + }, + { name = 'gsub: (limited to 0 replacements)', func = mw.ustring.gsub, + args = { 'áá', 'á', 'X', 0 }, + expect = { 'áá', 0 } + }, + { name = 'gsub: (string 1)', func = mw.ustring.gsub, + args = { str2, 'f%a+', 'X' }, + expect = { 'X bar X X baz X X X', 6 } + }, + { name = 'gsub: (string 2)', func = mw.ustring.gsub, + args = { str3, 'f%a+', 'X' }, + expect = { '??? X bar X X baz X X X ok?', 6 } + }, + { name = 'gsub: (string 3)', func = mw.ustring.gsub, + args = { str2, 'f%a+', 'X', 3 }, + expect = { 'X bar X X baz foooo foofoo fo', 3 } + }, + { name = 'gsub: (string 4)', func = mw.ustring.gsub, + args = { str3, 'f%a+', 'X', 3 }, + expect = { '??? X bar X X baz foooo foofoo fo ok?', 3 } + }, + { name = 'gsub: (string 5)', func = mw.ustring.gsub, + args = { 'foo; fóó', '(f)(%a+)', '%%0=%0 %%1=%1 %%2=%2' }, + expect = { '%0=foo %1=f %2=oo; %0=fóó %1=f %2=óó', 2 } + }, + { name = 'gsub: (anchored)', func = mw.ustring.gsub, + args = { 'foofoofoo foo', '^foo', 'X' }, + expect = { 'Xfoofoo foo', 1 } + }, + { name = 'gsub: (table 1)', func = mw.ustring.gsub, + args = { str2, 'f%a+', { foo = 'X', ['főó'] = 'Y', ['foó'] = 'Z' } }, + expect = { 'X bar Y Z baz foooo foofoo fo', 6 } + }, + { name = 'gsub: (table 2)', func = mw.ustring.gsub, + args = { str3, 'f%a+', { foo = 'X', ['főó'] = 'Y', ['foó'] = 'Z' } }, + expect = { '??? X bar Y Z baz foooo foofoo fo ok?', 6 } + }, + { name = 'gsub: (table 3)', func = mw.ustring.gsub, + args = { str2, 'f%a+', { ['főó'] = 'Y', ['foó'] = 'Z' }, 1 }, + expect = { str2, 1 } + }, + { name = 'gsub: (inverted zero character class)', func = mw.ustring.gsub, + args = { "ó", '%Z', 'repl' }, + expect = { 'repl', 1 } + }, + { name = 'gsub: (single dot pattern at end)', func = mw.ustring.gsub, + args = { "ó", '.', 'repl' }, + expect = { 'repl', 1 } + }, + { name = 'gsub: (single dot pattern at end + leading)', func = mw.ustring.gsub, + args = { 'fó', 'f.', 'repl' }, + expect = { 'repl', 1 } + }, + { name = 'gsub: (dot pattern)', func = mw.ustring.gsub, + args = { 'f ó b', 'f . b', 'repl' }, + expect = { 'repl', 1 } + }, + { name = 'gsub: (dot pattern with +)', func = mw.ustring.gsub, + args = { 'f óóó b', 'f .+ b', 'repl' }, + expect = { 'repl', 1 } + }, + { name = 'gsub: (dot pattern with -)', func = mw.ustring.gsub, + args = { 'f óóó b', 'f .- b', 'repl' }, + expect = { 'repl', 1 } + }, + { name = 'gsub: (dot pattern with *)', func = mw.ustring.gsub, + args = { 'f óóó b', 'f .* b', 'repl' }, + expect = { 'repl', 1 } + }, + { name = 'gsub: (function 1)', func = mw.ustring.gsub, + args = { str2, 'f%a+', function(m) if m == 'fo' then return nil end return '-' .. mw.ustring.upper(m) .. '-' end }, + expect = { '-FOO- bar -FŐÓ- -FOÓ- baz -FOOOO- -FOOFOO- fo', 6 } + }, + { name = 'gsub: (function 2)', func = mw.ustring.gsub, + args = { str3, 'f%a+', function(m) if m == 'fo' then return nil end return '-' .. mw.ustring.upper(m) .. '-' end }, + expect = { '??? -FOO- bar -FŐÓ- -FOÓ- baz -FOOOO- -FOOFOO- fo ok?', 6 } + }, + { name = 'gsub: invalid replacement string', func = mw.ustring.gsub, + args = { 'foo; fóó', '(%a+)', '%2' }, + expect = "invalid capture index %2 in replacement string" + }, + { name = 'gsub: passing numbers instead of strings (1)', func = mw.ustring.gsub, + args = { 12345, '[33]', '9' }, + expect = { '12945', 1 } + }, + { name = 'gsub: passing numbers instead of strings (2)', func = mw.ustring.gsub, + args = { '12345', 3, '9' }, + expect = { '12945', 1 } + }, + { name = 'gsub: passing numbers instead of strings (3)', func = mw.ustring.gsub, + args = { '12345', '[33]', 9 }, + expect = { '12945', 1 } + }, + + { name = 'gcodepoint: basic test', func = mw.ustring.gcodepoint, + args = { str1 }, + expect = { { 0 }, { 0x7f }, { 0x80 }, { 0x7ff }, { 0x800 }, { 0xffff }, { 0x10000 }, { 0x10ffff } }, + type = 'Iterator' + }, + { name = 'gcodepoint: (4)', func = mw.ustring.gcodepoint, + args = { str1, 4 }, + expect = { { 0x7ff }, { 0x800 }, { 0xffff }, { 0x10000 }, { 0x10ffff } }, + type = 'Iterator' + }, + { name = 'gcodepoint: (4, -2)', func = mw.ustring.gcodepoint, + args = { str1, 4, -2 }, + expect = { { 0x7ff }, { 0x800 }, { 0xffff }, { 0x10000 } }, + type = 'Iterator' + }, + { name = 'gcodepoint: (4, 3)', func = mw.ustring.gcodepoint, + args = { str1, 4, 3 }, + expect = {}, + type = 'Iterator' + }, + { name = 'gcodepoint: (1, 0)', func = mw.ustring.gcodepoint, + args = { str1, 1, 0 }, + expect = {}, + type = 'Iterator' + }, + { name = 'gcodepoint: (9, 9)', func = mw.ustring.gcodepoint, + args = { str1, 9, 9 }, + expect = {}, + type = 'Iterator' + }, + { name = 'gcodepoint: really long string', func = testLongGcodepoint, + args = {}, + expect = { { + [1] = 0x66, [2] = 0x151, [3] = 0xf3, [4] = 0x20, + [9997] = 0x66, [9998] = 0x151, [9999] = 0xf3, [10000] = 0x20, + } }, + }, + + { name = 'gmatch: test string 1', func = mw.ustring.gmatch, + args = { str2, 'f%a+' }, + expect = { { 'foo' }, { 'főó' }, { 'foó' }, { 'foooo' }, { 'foofoo' }, { 'fo' } }, + type = 'Iterator' + }, + { name = 'gmatch: test string 2', func = mw.ustring.gmatch, + args = { str3, 'f%a+' }, + expect = { { 'foo' }, { 'főó' }, { 'foó' }, { 'foooo' }, { 'foofoo' }, { 'fo' } }, + type = 'Iterator' + }, + { name = 'gmatch: anchored', func = mw.ustring.gmatch, + args = { "fóó1 ^fóó2 fóó3 ^fóó4", '^fóó%d+' }, + expect = { { "^fóó2" }, { "^fóó4" } }, + type = 'Iterator' + }, + + { name = 'find: Pure-lua version, non-native error message', func = mw.ustring.find, + args = { "fóó", '[]' }, + expect = "Missing close-bracket for character set beginning at pattern character 1" + }, + { name = 'match: Pure-lua version, non-native error message', func = mw.ustring.match, + args = { "fóó", '[]' }, + expect = "Missing close-bracket for character set beginning at pattern character 1" + }, + { name = 'gsub: Pure-lua version, non-native error message', func = mw.ustring.gsub, + args = { "fóó", '[]', '' }, + expect = "Missing close-bracket for character set beginning at pattern character 1" + }, + + { name = 'string length limit', + func = function () + local s = string.rep( "x", mw.ustring.maxStringLength + 1 ) + local ret = { mw.ustring.gsub( s, 'a', 'b' ) } + -- So the output isn't insanely long + ret[1] = string.gsub( ret[1], 'xxxxx(x*)', function ( m ) + return 'xxxxx[snip ' .. #m .. ' more]' + end ) + return unpack( ret ) + end, + expect = "bad argument #1 to 'gsub' (string is longer than " .. mw.ustring.maxStringLength .. " bytes)" + }, + { name = 'pattern length limit', + func = function () + local pattern = string.rep( "x", mw.ustring.maxPatternLength + 1 ) + return mw.ustring.gsub( 'a', pattern, 'b' ) + end, + expect = "bad argument #2 to 'gsub' (pattern is longer than " .. mw.ustring.maxPatternLength .. " bytes)" + }, +} ) |