mediawiki-extensions-Scribunto/tests/phpunit/engines/LuaCommon/LuaCommonTest.php
Kunal Mehta a839ba855d Add mw.loadJsonData()
Backporting this so the LTS release has forwards compatibility with
Wikipedia templates.

mw.loadData() allows for optimizing the loading Lua tables by requiring
only one parse and lookup. However it's often easier for people to
write/maintain bulk data in JSON rather than Lua tables.

mw.loadJsonData() has roughly the same characteristics as mw.loadData()
and it can be used on JSON content model pages in any namespace.

As noted on the linked bug report, it's possible to already implement
this by writing a wrapper Lua module that loads and parses the JSON
content. But that requires a dummy module for each JSON page, which is
just annoying and inconvenient.

Test cases are copied from the mw.loadData() ones, with a few omissions
for syntax not supported in JSON (e.g. NaN, infinity, etc.).

Bug: T217500
Change-Id: I1b35ad27a37b94064707bb8c9b7108c7078ed4d1
(cherry picked from commit 1000d322e5)
2022-11-07 07:34:42 +00:00

900 lines
28 KiB
PHP

<?php
use MediaWiki\Extension\Scribunto\ScribuntoException;
/**
* @covers \MediaWiki\Extension\Scribunto\ScribuntoEngineBase
* @covers Scribunto_LuaEngine
* @covers \MediaWiki\Extension\Scribunto\Engines\LuaStandalone\LuaStandaloneEngine
* @covers \MediaWiki\Extension\Scribunto\Engines\LuaSandbox\LuaSandboxEngine
* @covers Scribunto_LuaInterpreter
* @covers \MediaWiki\Extension\Scribunto\Engines\LuaStandalone\LuaStandaloneInterpreter
* @covers \MediaWiki\Extension\Scribunto\Engines\LuaSandbox\LuaSandboxInterpreter
* @group Database
*/
class Scribunto_LuaCommonTest extends Scribunto_LuaEngineTestBase {
/** @inheritDoc */
protected static $moduleName = 'CommonTests';
/** @var string[] */
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(): void {
parent::setUp();
// Register libraries for self::testPHPLibrary()
$this->setTemporaryHook(
'ScribuntoExternalLibraries',
static function ( $engine, &$libs ) {
$libs += [
'CommonTestsLib' => [
'class' => Scribunto_LuaCommonTestsLibrary::class,
'deferLoad' => true,
],
'CommonTestsFailLib' => [
'class' => Scribunto_LuaCommonTestsFailLibrary::class,
'deferLoad' => true,
],
];
}
);
$status = $this->editPage(
Title::makeTitle( NS_MODULE, 'CommonTests-data.json' ),
file_get_contents( __DIR__ . '/CommonTests-data.json' )
);
if ( !$status->isOK() ) {
throw new \Exception( "Failed to create Module:CommonTests-data.json: " . $status->getWikitext() );
}
// Create a non-JSON/Module page for loadJsonData()
$status = $this->editPage(
Title::makeTitle( NS_HELP, 'Foo' ),
'help'
);
if ( !$status->isOK() ) {
throw new \Exception( "Failed to create Help:Foo: " . $status->getWikitext() );
}
// 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->assertEmpty( $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( static 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 testLoadJsonDataLoadedOnce() {
$this->extraModules['Module:TestLoadDataLoadedOnce'] = '
local data = mw.loadJsonData( "Module:CommonTests-data.json" )
return {
foo = function() end,
}
';
$engine = $this->getEngine();
$interpreter = $engine->getInterpreter();
$frame = $engine->getParser()->getPreprocessor()->newFrame();
// Make sure JSON data 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, $this->templateLoadCounts['Module:CommonTests-data.json'],
'JSON data was loaded more than once'
);
}
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->getStripState()->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->getStripState()->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->getStripState()->unstripBoth( $ret['return'] ),
'extensionTag works for {{#tag:pre|foo|style=margin-left: 1.6em}}'
);
// Test calling a non-existent function
try {
$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->assertSame( 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->assertSame( 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->assertSame( 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', static 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->getStripState()->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->getStripState()->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->getStripState()->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"
);
}
}
public function testGetCurrentFrameAtModuleScopeT234368() {
$engine = $this->getEngine();
$parser = $engine->getParser();
$pp = $parser->getPreprocessor();
$this->extraModules['Module:Outer'] = '
local p = {}
function p.echo( frame )
return "(Outer: 1=" .. frame.args[1] .. ", 2=" .. frame.args[2] .. ")"
end
return p
';
$this->extraModules['Module:Inner'] = '
local p = {}
local f = mw.getCurrentFrame()
local name = f:getTitle()
local arg1 = f.args[1]
function p.test( frame )
local f2 = mw.getCurrentFrame()
return "(Inner: mod_name=" .. name .. ", mod_1=" .. arg1 .. ", name=" .. f2:getTitle() ..
", 1=".. f2.args[1] .. ")"
end
return p
';
$frame = $pp->newFrame();
$text = $frame->expand( $pp->preprocessToObj(
"{{#invoke:Outer|echo|oarg|{{#invoke:Inner|test|iarg}}}}"
) );
$text = $parser->getStripState()->unstripBoth( $text );
$this->assertSame(
'(Outer: 1=oarg, 2=(Inner: mod_name=Module:Inner, mod_1=iarg, name=Module:Inner, 1=iarg))',
$text
);
}
public function testNonUtf8Errors() {
$engine = $this->getEngine();
$parser = $engine->getParser();
$this->extraModules['Module:T208689'] = '
local p = {}
p["foo\255bar"] = function ()
error( "error\255bar" )
end
p.foo = function ()
p["foo\255bar"]()
end
return p
';
// As via the API
try {
$engine->runConsole( [
'prevQuestions' => [],
'question' => 'p.foo()',
'title' => Title::newFromText( 'Module:T208689' ),
'content' => $this->extraModules['Module:T208689'],
] );
$this->fail( 'Expected exception not thrown' );
} catch ( ScribuntoException $e ) {
$this->assertTrue( mb_check_encoding( $e->getMessage(), 'UTF-8' ), 'Message is UTF-8' );
$this->assertTrue( mb_check_encoding( $e->getScriptTraceHtml(), 'UTF-8' ), 'Message is UTF-8' );
}
// Via the parser
$text = $parser->recursiveTagParseFully( '{{#invoke:T208689|foo}}' );
$this->assertTrue( mb_check_encoding( $text, 'UTF-8' ), 'Parser output is UTF-8' );
$vars = $parser->getOutput()->getJsConfigVars();
$this->assertArrayHasKey( 'ScribuntoErrors', $vars );
foreach ( $vars['ScribuntoErrors'] as $err ) {
$this->assertTrue( mb_check_encoding( $err, 'UTF-8' ), 'JS config vars are UTF-8' );
}
}
public function testT236092() {
$engine = $this->getEngine();
$parser = $engine->getParser();
$pp = $parser->getPreprocessor();
$this->extraModules['Module:T236092'] = '
local p = {}
p.foo = mw.isSubsting
return p
';
$frame = $pp->newFrame();
$text = $frame->expand( $pp->preprocessToObj( ">{{#invoke:T236092|foo}}<" ) );
$text = $parser->getStripState()->unstripBoth( $text );
$this->assertSame( '>false<', $text );
}
public function testAddWarning() {
$engine = $this->getEngine();
$parser = $engine->getParser();
$pp = $parser->getPreprocessor();
$this->extraModules['Module:TestAddWarning'] = '
local p = {}
p.foo = function ()
mw.addWarning( "Don\'t panic!" )
return "ok"
end
return p
';
$frame = $pp->newFrame();
$text = $frame->expand( $pp->preprocessToObj( ">{{#invoke:TestAddWarning|foo}}<" ) );
$text = $parser->getStripState()->unstripBoth( $text );
$this->assertSame( '>ok<', $text );
$this->assertSame( [ 'Script warning: Don\'t panic!' ], $parser->getOutput()->getWarnings() );
}
}