56 $this->mergeMwGlobalArrayValue(
'wgHooks', [
57 'ScribuntoExternalLibraries' => [
61 'class' =>
'Scribunto_LuaCommonTestsLibrary',
64 'CommonTestsFailLib' => [
65 'class' =>
'Scribunto_LuaCommonTestsFailLibrary',
74 $this->
getEngine()->getParser()->getOptions()->setExpensiveParserFunctionLimit( 10 );
77 $interpreter = $this->
getEngine()->getInterpreter();
78 $interpreter->callFunction( $interpreter->loadString(
79 'mw.makeProtectedEnvFuncsForTest = mw.makeProtectedEnvFuncs',
'fortest'
84 return parent::getTestModules() + [
85 'CommonTests' => __DIR__ .
'/CommonTests.lua',
86 'CommonTests-data' => __DIR__ .
'/CommonTests-data.lua',
87 'CommonTests-data-fail1' => __DIR__ .
'/CommonTests-data-fail1.lua',
88 'CommonTests-data-fail2' => __DIR__ .
'/CommonTests-data-fail2.lua',
89 'CommonTests-data-fail3' => __DIR__ .
'/CommonTests-data-fail3.lua',
90 'CommonTests-data-fail4' => __DIR__ .
'/CommonTests-data-fail4.lua',
91 'CommonTests-data-fail5' => __DIR__ .
'/CommonTests-data-fail5.lua',
96 $interpreter = $this->
getEngine()->getInterpreter();
98 list( $actualGlobals ) = $interpreter->callFunction(
99 $interpreter->loadString(
100 'local t = {} for k in pairs( _G ) do t[#t+1] = k end return t',
105 $leakedGlobals = array_diff( $actualGlobals, self::$allowedGlobals );
106 $this->assertEquals( 0, count( $leakedGlobals ),
107 'The following globals are leaked: ' . implode(
' ', $leakedGlobals )
113 $frame =
$engine->getParser()->getPreprocessor()->newFrame();
116 $this->extraModules[
$title->getFullText()] =
'
120 local lib = require( "CommonTestsLib" )
121 return table.concat( { lib.test() }, "; " )
124 function p.setVal( frame )
125 local lib = require( "CommonTestsLib" )
126 lib.val = frame.args[1]
127 lib.foobar.val = frame.args[1]
131 local lib = require( "CommonTestsLib" )
132 return tostring( lib.val ), tostring( lib.foobar.val )
135 function p.getSetVal( frame )
140 function p.checkPackage()
142 ret[1] = package.loaded["CommonTestsLib"] == nil
143 require( "CommonTestsLib" )
144 ret[2] = package.loaded["CommonTestsLib"] ~= nil
145 return ret[1], ret[2]
148 function p.libSetVal( frame )
149 local lib = require( "CommonTestsLib" )
150 return lib.setVal( frame )
153 function p.libGetVal()
154 local lib = require( "CommonTestsLib" )
163 $ret = $module->invoke(
'test', $frame->newChild() );
164 $this->assertSame(
'Test option; Test function', $ret,
165 'Library can be loaded and called' );
167 # Test package.loaded
169 $ret = $module->invoke(
'checkPackage', $frame->newChild() );
170 $this->assertSame(
'truetrue', $ret,
171 'package.loaded is right on the first call' );
172 $ret = $module->invoke(
'checkPackage', $frame->newChild() );
173 $this->assertSame(
'truetrue', $ret,
174 'package.loaded is right on the second call' );
176 # Test caching for require
177 $args =
$engine->getParser()->getPreprocessor()->newPartNodeArray( [ 1 =>
'cached' ] );
178 $ret = $module->invoke(
'getSetVal', $frame->newChild(
$args ) );
179 $this->assertSame(
'cachedcached', $ret,
180 'same loaded table is returned by multiple require calls' );
182 # Test no data communication between invokes
184 $args =
$engine->getParser()->getPreprocessor()->newPartNodeArray( [ 1 =>
'fail' ] );
185 $module->invoke(
'setVal', $frame->newChild(
$args ) );
186 $ret = $module->invoke(
'getVal', $frame->newChild() );
187 $this->assertSame(
'nilnope', $ret,
188 'same loaded table is not shared between invokes' );
190 # Test that the library isn't being recreated between invokes
192 $ret = $module->invoke(
'libGetVal', $frame->newChild() );
193 $this->assertSame(
'nil', $ret,
'sanity check' );
194 $args =
$engine->getParser()->getPreprocessor()->newPartNodeArray( [ 1 =>
'ok' ] );
195 $module->invoke(
'libSetVal', $frame->newChild(
$args ) );
198 $ret = $module->invoke(
'libGetVal', $frame->newChild() );
199 $this->assertSame(
'ok', $ret,
200 'library is not recreated between invokes' );
205 $interpreter =
$engine->getInterpreter();
207 $interpreter->callFunction(
208 $interpreter->loadString(
'string.testModuleStringExtend = "ok"',
'extendstring' )
210 $ret = $interpreter->callFunction(
211 $interpreter->loadString(
'return ("").testModuleStringExtend',
'teststring1' )
213 $this->assertSame( [
'ok' ], $ret,
'string can be extended' );
215 $this->extraModules[
'Module:testModuleStringExtend'] =
'
217 test = function() return ("").testModuleStringExtend end
220 $module =
$engine->fetchModuleFromParser(
223 $ret = $interpreter->callFunction(
224 $engine->executeModule( $module->getInitChunk(),
'test', null )
226 $this->assertSame( [
'ok' ], $ret,
'string extension can be used from module' );
228 $this->extraModules[
'Module:testModuleStringExtend2'] =
'
231 string.testModuleStringExtend = "fail"
232 return ("").testModuleStringExtend
236 $module =
$engine->fetchModuleFromParser(
239 $ret = $interpreter->callFunction(
240 $engine->executeModule( $module->getInitChunk(),
'test', null )
242 $this->assertSame( [
'ok' ], $ret,
'string extension cannot be modified from module' );
243 $ret = $interpreter->callFunction(
244 $interpreter->loadString(
'return string.testModuleStringExtend',
'teststring2' )
246 $this->assertSame( [
'ok' ], $ret,
'string extension cannot be modified from module' );
249 'prevQuestions' => [],
250 'question' =>
'=("").testModuleStringExtend',
251 'content' =>
'return {}',
254 $this->assertSame(
'ok', $ret[
'return'],
'string extension can be used from console' );
257 'prevQuestions' => [
'string.fail = "fail"' ],
258 'question' =>
'=("").fail',
259 'content' =>
'return {}',
262 $this->assertSame(
'nil', $ret[
'return'],
'string cannot be extended from console' );
265 'prevQuestions' => [
'string.testModuleStringExtend = "fail"' ],
266 'question' =>
'=("").testModuleStringExtend',
267 'content' =>
'return {}',
270 $this->assertSame(
'ok', $ret[
'return'],
'string extension cannot be modified from console' );
271 $ret = $interpreter->callFunction(
272 $interpreter->loadString(
'return string.testModuleStringExtend',
'teststring3' )
274 $this->assertSame( [
'ok' ], $ret,
'string extension cannot be modified from console' );
276 $interpreter->callFunction(
277 $interpreter->loadString(
'string.testModuleStringExtend = nil',
'unextendstring' )
283 $interpreter =
$engine->getInterpreter();
284 $frame =
$engine->getParser()->getPreprocessor()->newFrame();
287 $interpreter->callFunction(
288 $interpreter->loadString(
'mw.markLoaded = ...',
'fortest' ),
289 $interpreter->wrapPHPFunction(
function () use ( &$loadcount ) {
293 $this->extraModules[
'Module:TestLoadDataLoadedOnce-data'] =
'
297 $this->extraModules[
'Module:TestLoadDataLoadedOnce'] =
'
298 local data = mw.loadData( "Module:TestLoadDataLoadedOnce-data" )
300 foo = function() end,
302 return tostring( package.loaded["Module:TestLoadDataLoadedOnce-data"] )
309 for ( $i = 0; $i < 10; $i++ ) {
311 $module->invoke(
'foo', $frame->newChild() );
313 $this->assertSame( 1, $loadcount,
'data module was loaded more than once' );
316 $this->assertSame(
'nil', $module->invoke(
'bar', $frame ),
317 'data module was stored in module\'s package.loaded'
319 $this->assertSame( [
'nil' ],
320 $interpreter->callFunction( $interpreter->loadString(
321 'return tostring( package.loaded["Module:TestLoadDataLoadedOnce-data"] )',
'getLoaded'
323 'data module was stored in top level\'s package.loaded'
331 'prevQuestions' => [],
332 'question' =>
'=mw.getCurrentFrame()',
333 'content' =>
'return {}',
336 $this->assertSame(
'table', $ret[
'return'],
'frames can be used in the console' );
339 'prevQuestions' => [],
340 'question' =>
'=mw.getCurrentFrame():newChild{}',
341 'content' =>
'return {}',
344 $this->assertSame(
'table', $ret[
'return'],
'child frames can be created' );
348 'f = mw.getCurrentFrame():newChild{ args = { "ok" } }',
349 'f2 = f:newChild{ args = {} }'
351 'question' =>
'=f2:getParent().args[1], f2:getParent():getParent()',
352 'content' =>
'return {}',
355 $this->assertSame(
"ok\ttable", $ret[
'return'],
'child frames have correct parents' );
360 $parser =
$engine->getParser();
363 'prevQuestions' => [],
364 'content' =>
'return {}',
370 'question' =>
'=mw.getCurrentFrame():callParserFunction{
371 name = "urlencode", args = { "x x", "wiki" }
374 $this->assertSame(
"x_x", $ret[
'return'],
375 'callParserFunction works for {{urlencode:x x|wiki}} (named args w/table)'
379 'question' =>
'=mw.getCurrentFrame():callParserFunction{
380 name = "urlencode", args = "x x"
383 $this->assertSame(
"x+x", $ret[
'return'],
384 'callParserFunction works for {{urlencode:x x}} (named args w/scalar)'
388 'question' =>
'=mw.getCurrentFrame():callParserFunction( "urlencode", { "x x", "wiki" } )',
390 $this->assertSame(
"x_x", $ret[
'return'],
391 'callParserFunction works for {{urlencode:x x|wiki}} (positional args w/table)'
395 'question' =>
'=mw.getCurrentFrame():callParserFunction( "urlencode", "x x", "wiki" )',
397 $this->assertSame(
"x_x", $ret[
'return'],
398 'callParserFunction works for {{urlencode:x x|wiki}} (positional args w/scalars)'
402 'question' =>
'=mw.getCurrentFrame():callParserFunction{
403 name = "urlencode:x x", args = { "wiki" }
406 $this->assertSame(
"x_x", $ret[
'return'],
407 'callParserFunction works for {{urlencode:x x|wiki}} (colon in name, named args w/table)'
411 'question' =>
'=mw.getCurrentFrame():callParserFunction{
412 name = "urlencode:x x", args = "wiki"
415 $this->assertSame(
"x_x", $ret[
'return'],
416 'callParserFunction works for {{urlencode:x x|wiki}} (colon in name, named args w/scalar)'
420 'question' =>
'=mw.getCurrentFrame():callParserFunction( "urlencode:x x", { "wiki" } )',
422 $this->assertSame(
"x_x", $ret[
'return'],
423 'callParserFunction works for {{urlencode:x x|wiki}} (colon in name, positional args w/table)'
427 'question' =>
'=mw.getCurrentFrame():callParserFunction( "urlencode:x x", "wiki" )',
429 $this->assertSame(
"x_x", $ret[
'return'],
430 'callParserFunction works for {{urlencode:x x|wiki}} (colon in name, positional args w/scalars)'
435 'question' =>
'=mw.getCurrentFrame():callParserFunction( "#tag:pre",
436 { "foo", style = "margin-left: 1.6em" }
440 '<pre style="margin-left: 1.6em">foo</pre>',
441 $parser->mStripState->unstripBoth( $ret[
'return'] ),
442 'callParserFunction works for {{#tag:pre|foo|style=margin-left: 1.6em}}'
447 'question' =>
'=mw.getCurrentFrame():extensionTag( "pre", "foo",
448 { style = "margin-left: 1.6em" }
452 '<pre style="margin-left: 1.6em">foo</pre>',
453 $parser->mStripState->unstripBoth( $ret[
'return'] ),
454 'extensionTag works for {{#tag:pre|foo|style=margin-left: 1.6em}}'
458 'question' =>
'=mw.getCurrentFrame():extensionTag{ name = "pre", content = "foo",
459 args = { style = "margin-left: 1.6em" }
463 '<pre style="margin-left: 1.6em">foo</pre>',
464 $parser->mStripState->unstripBoth( $ret[
'return'] ),
465 'extensionTag works for {{#tag:pre|foo|style=margin-left: 1.6em}}'
471 'question' =>
'=mw.getCurrentFrame():callParserFunction{
472 name = "thisDoesNotExist", args = { "" }
475 $this->fail(
"Expected LuaError not thrown for nonexistent parser function" );
478 'Lua error: callParserFunction: function "thisDoesNotExist" was not found.',
480 'callParserFunction correctly errors for nonexistent function'
487 $frame =
$engine->getParser()->getPreprocessor()->newFrame();
489 $this->extraModules[
'Module:Bug62291'] =
'
492 return table.concat( {
493 math.random(), math.random(), math.random(), math.random(), math.random()
499 t[2] = mw.getCurrentFrame():preprocess( "{{#invoke:Bug62291|bar2}}" )
501 return table.concat( t, "; " )
513 $r1 = $module->invoke(
'foo', $frame->newChild() );
514 $r2 = $module->invoke(
'foo', $frame->newChild() );
515 $this->assertSame( $r1, $r2,
'Multiple invokes returned different sets of random numbers' );
518 $r1 = $module->invoke(
'bar', $frame->newChild() );
519 $r = explode(
'; ', $r1 );
520 $this->assertNotSame( $r[0], $r[2],
'Recursive invoke reset PRNG' );
521 $this->assertSame(
'bar2 called', $r[1],
'Sanity check failed' );
524 $r2 = $module->invoke(
'bar', $frame->newChild() );
525 $this->assertSame( $r1, $r2,
526 'Multiple invokes with recursive invoke returned different sets of random numbers' );
531 $pp =
$engine->getParser()->getPreprocessor();
533 $this->extraModules[
'Module:DateTime'] =
'
536 return os.date( "%d" )
539 return os.date( "%p" )
542 return os.date( "%H" )
545 return os.date( "%M" )
548 return os.date( "%S" )
551 return os.date( "*t" )
553 function p.tablesec()
554 return os.date( "*t" ).sec
559 function p.specificDateAndTime()
560 return os.date("%S", os.time{year = 2013, month = 1, day = 1})
568 $frame = $pp->newFrame();
569 $module->invoke(
'day', $frame );
570 $this->assertNotNull( $frame->getTTL(),
'TTL must be set when day is requested' );
571 $this->assertLessThanOrEqual( 86400, $frame->getTTL(),
572 'TTL must not exceed 1 day when day is requested' );
574 $frame = $pp->newFrame();
575 $module->invoke(
'AMPM', $frame );
576 $this->assertNotNull( $frame->getTTL(),
'TTL must be set when AM/PM is requested' );
577 $this->assertLessThanOrEqual( 43200, $frame->getTTL(),
578 'TTL must not exceed 12 hours when AM/PM is requested' );
580 $frame = $pp->newFrame();
581 $module->invoke(
'hour', $frame );
582 $this->assertNotNull( $frame->getTTL(),
'TTL must be set when hour is requested' );
583 $this->assertLessThanOrEqual( 3600, $frame->getTTL(),
584 'TTL must not exceed 1 hour when hours are requested' );
586 $frame = $pp->newFrame();
587 $module->invoke(
'minute', $frame );
588 $this->assertNotNull( $frame->getTTL(),
'TTL must be set when minutes are requested' );
589 $this->assertLessThanOrEqual( 60, $frame->getTTL(),
590 'TTL must not exceed 1 minute when minutes are requested' );
592 $frame = $pp->newFrame();
593 $module->invoke(
'second', $frame );
594 $this->assertEquals( 1, $frame->getTTL(),
595 'TTL must be equal to 1 second when seconds are requested' );
597 $frame = $pp->newFrame();
598 $module->invoke(
'table', $frame );
599 $this->assertNull( $frame->getTTL(),
600 'TTL must not be set when os.date( "*t" ) is called but no values are looked at' );
602 $frame = $pp->newFrame();
603 $module->invoke(
'tablesec', $frame );
604 $this->assertEquals( 1, $frame->getTTL(),
605 'TTL must be equal to 1 second when seconds are requested from a table' );
607 $frame = $pp->newFrame();
608 $module->invoke(
'time', $frame );
609 $this->assertEquals( 1, $frame->getTTL(),
610 'TTL must be equal to 1 second when os.time() is called' );
612 $frame = $pp->newFrame();
613 $module->invoke(
'specificDateAndTime', $frame );
614 $this->assertNull( $frame->getTTL(),
615 'TTL must not be set when os.date() or os.time() are called with a specific time' );
623 $parser =
$engine->getParser();
624 $pp = $parser->getPreprocessor();
627 $parser->setHook(
'scribuntocount',
function ( $str, $argv, $parser, $frame ) use ( &$count ) {
628 $frame->setVolatile();
632 $this->extraModules[
'Template:ScribuntoTestVolatileCaching'] =
'<scribuntocount/>';
633 $this->extraModules[
'Module:TestVolatileCaching'] =
'
635 preprocess = function ( frame )
636 return frame:preprocess( "<scribuntocount/>" )
638 extensionTag = function ( frame )
639 return frame:extensionTag( "scribuntocount" )
641 expandTemplate = function ( frame )
642 return frame:expandTemplate{ title = "ScribuntoTestVolatileCaching" }
647 $frame = $pp->newFrame();
649 $wikitext =
"{{#invoke:TestVolatileCaching|$func}}";
650 $text = $frame->expand( $pp->preprocessToObj(
"$wikitext $wikitext" ) );
651 $text = $parser->mStripState->unstripBoth( $text );
652 $this->assertTrue( $frame->isVolatile(),
"Frame is marked volatile" );
653 $this->assertEquals(
'1 2', $text,
"Volatile wikitext was not cached" );
660 [
'expandTemplate' ],
666 $parser =
$engine->getParser();
667 $pp = $parser->getPreprocessor();
669 $this->extraModules[
'Module:Bug65687'] =
'
671 test = function ( frame )
672 return mw.loadData( "Module:Bug65687-LD" )[1]
676 $this->extraModules[
'Module:Bug65687-LD'] =
'return { mw.getCurrentFrame().args[1] or "ok" }';
678 $frame = $pp->newFrame();
679 $text = $frame->expand( $pp->preprocessToObj(
"{{#invoke:Bug65687|test|foo}}" ) );
680 $text = $parser->mStripState->unstripBoth( $text );
681 $this->assertEquals(
'ok', $text,
'mw.loadData allowed access to frame args' );
686 $parser =
$engine->getParser();
687 $pp = $parser->getPreprocessor();
689 $this->extraModules[
'Module:Bug67498-directly'] =
'
690 local f = mw.getCurrentFrame()
691 local f2 = f and f.args[1] or "<none>"
694 test = function ( frame )
695 return ( f and f.args[1] or "<none>" ) .. " " .. f2
699 $this->extraModules[
'Module:Bug67498-statically'] =
'
700 local M = require( "Module:Bug67498-directly" )
702 test = function ( frame )
703 return M.test( frame )
707 $this->extraModules[
'Module:Bug67498-dynamically'] =
'
709 test = function ( frame )
710 local M = require( "Module:Bug67498-directly" )
711 return M.test( frame )
716 foreach ( [
'directly',
'statically',
'dynamically' ] as $how ) {
717 $frame = $pp->newFrame();
718 $text = $frame->expand( $pp->preprocessToObj(
719 "{{#invoke:Bug67498-$how|test|foo}} -- {{#invoke:Bug67498-$how|test|bar}}"
721 $text = $parser->mStripState->unstripBoth( $text );
722 $text = explode(
' -- ', $text );
723 $this->assertEquals(
'foo foo', $text[0],
724 "mw.getCurrentFrame() failed from a module loaded $how"
726 $this->assertEquals(
'bar bar', $text[1],
727 "mw.getCurrentFrame() cached the frame from a module loaded $how"
734 $parser =
$engine->getParser();
736 $this->extraModules[
'Module:T208689'] =
'
739 p["foo\255bar"] = function ()
740 error( "error\255bar" )
753 'prevQuestions' => [],
754 'question' =>
'p.foo()',
756 'content' => $this->extraModules[
'Module:T208689'],
758 $this->fail(
'Expected exception not thrown' );
760 $this->assertTrue( mb_check_encoding( $e->getMessage(),
'UTF-8' ),
'Message is UTF-8' );
761 $this->assertTrue( mb_check_encoding( $e->
getScriptTraceHtml(),
'UTF-8' ),
'Message is UTF-8' );
765 $text = $parser->recursiveTagParseFully(
'{{#invoke:T208689|foo}}' );
766 $this->assertTrue( mb_check_encoding( $text,
'UTF-8' ),
'Parser output is UTF-8' );
767 $vars = $parser->getOutput()->getJsConfigVars();
768 $this->assertArrayHasKey(
'ScribuntoErrors', $vars );
769 foreach ( $vars[
'ScribuntoErrors'] as $err ) {
770 $this->assertTrue( mb_check_encoding( $err,
'UTF-8' ),
'JS config vars are UTF-8' );
776 public function register() {
778 'test' => [ $this,
'test' ],
781 'test' =>
'Test option',
784 return $this->
getEngine()->registerInterface( __DIR__ .
'/CommonTests-lib.lua', $lib, $opts );
788 return [
'Test function' ];
794 throw new MWException(
'deferLoad library that is never required was loaded anyway' );
797 public function register() {