summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTest.php
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTest.php')
-rw-r--r--www/wiki/extensions/Scribunto/tests/phpunit/engines/LuaCommon/CommonTest.php751
1 files changed, 751 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() {
+ }
+}