summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon')
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTest.php751
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-data-fail1.lua2
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-data-fail2.lua4
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-data-fail3.lua4
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-data-fail4.lua4
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-data-fail5.lua4
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-data.lua20
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests-lib.lua30
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTests.lua387
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/HashLibraryTest.php13
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/HashLibraryTests.lua53
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/HtmlLibraryTest.php26
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/HtmlLibraryTests.lua359
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LanguageLibraryTest.php70
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LanguageLibraryTests.lua435
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LibraryUtilTest.php12
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LibraryUtilTests.lua220
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LuaDataProvider.php53
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LuaEngineTestBase.php291
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LuaEnvironmentComparisonTest.php114
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/LuaInterpreterTest.php164
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/MessageLibraryTest.php12
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/MessageLibraryTests.lua70
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/SiteLibraryTest.php12
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/SiteLibraryTests.lua182
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/TestFramework.lua246
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/TextLibraryTest.php41
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/TextLibraryTests.lua509
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/TitleLibraryTest.php167
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/TitleLibraryTests.lua420
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/UriLibraryTest.php26
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/UriLibraryTests.lua182
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/UstringLibraryNormalizationTests.lua33
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/UstringLibraryPureLuaTest.php35
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/UstringLibraryTest.php192
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/UstringLibraryTests.lua730
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&quot;rgh"></div>' }
+ },
+ { name = 'mw.html attribute escaping 1', func = testHelper, type='ToString',
+ args = { getEmptyTestDiv(), 'attr', 'foo', 'ble<rgh' },
+ expect = { '<div foo="ble&lt;rgh"></div>' }
+ },
+ { name = 'mw.html attribute escaping 2', func = testHelper, type='ToString',
+ args = { getEmptyTestDiv(), 'attr', 'foo', '<ble"&rgh>' },
+ expect = { '<div foo="&lt;ble&quot;&amp;rgh&gt;"></div>' }
+ },
+ { name = 'mw.html attribute escaping (CSS)', func = testHelper, type='ToString',
+ args = { getEmptyTestDiv(), 'css', 'mu"ha', 'ha"ha' },
+ expect = { '<div style="mu&quot;ha:ha&quot;ha"></div>' }
+ },
+ { name = 'mw.html attribute escaping (CSS raw)', func = testHelper, type='ToString',
+ args = { getEmptyTestDiv(), 'cssText', 'mu"ha:-ha"ha' },
+ expect = { '<div style="mu&quot;ha:-ha&quot;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&#039;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&#039;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 = {
+ { "&lrm;" },
+ { "&lrm;" },
+ { "&rlm;" },
+ "language code '[[bogus]]' is invalid",
+ }
+ },
+ { name = 'lang:getDirMarkEntity opposite', func = test_method,
+ args = { 'getDirMarkEntity', true },
+ expect = {
+ { "&rlm;" },
+ { "&rlm;" },
+ { "&lrm;" },
+ "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 = { '&lt;b&gt;foo&nbsp;&quot;bar&quot;&lt;/b&gt; &amp; &#039;baz&#039;' }
+ },
+ { name = 'encode charset',
+ func = mw.text.encode, args = { '<b>foo\194\160"bar"</b> & \'baz\'', 'aeiou' },
+ expect = { '<b>f&#111;&#111;\194\160"b&#97;r"</b> & \'b&#97;z\'' }
+ },
+
+ { name = 'decode',
+ func = mw.text.decode,
+ args = { '&lt;&gt;&amp;&quot; &#102;&#111;&#x6f; &#x0066;&#00111;&#x6F; &hearts; &amp;quot;' },
+ expect = { '<>&" foo foo &hearts; &quot;' }
+ },
+ { name = 'decode named',
+ func = mw.text.decode,
+ args = { '&lt;&gt;&amp;&quot; &#102;&#111;&#x6f; &#x0066;&#00111;&#x6F; &hearts; &amp;quot;', true },
+ expect = { '<>&" foo foo ♥ &quot;' }
+ },
+
+ { name = 'nowiki',
+ func = mw.text.nowiki,
+ args = { '*"&\'<=>[]{|}#*:;\n*\n#\n:\n;\nhttp://example.com:80/\nRFC 123, ISBN 456' },
+ expect = {
+ '&#42;&#34;&#38;&#39;&#60;&#61;&#62;&#91;&#93;&#123;&#124;&#125;#*:;' ..
+ '\n&#42;\n&#35;\n&#58;\n&#59;\nhttp&#58;//example.com:80/' ..
+ '\nRFC&#32;123, ISBN&#32;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)"
+ },
+} )