mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Scribunto
synced 2024-12-19 03:12:52 +00:00
1000d322e5
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
904 lines
28 KiB
PHP
904 lines
28 KiB
PHP
<?php
|
|
|
|
namespace MediaWiki\Extension\Scribunto\Tests\Engines\LuaCommon;
|
|
|
|
use MediaWiki\Extension\Scribunto\Engines\LuaCommon\LuaError;
|
|
use MediaWiki\Extension\Scribunto\ScribuntoException;
|
|
use Title;
|
|
|
|
/**
|
|
* @covers \MediaWiki\Extension\Scribunto\ScribuntoEngineBase
|
|
* @covers \MediaWiki\Extension\Scribunto\Engines\LuaCommon\LuaEngine
|
|
* @covers \MediaWiki\Extension\Scribunto\Engines\LuaStandalone\LuaStandaloneEngine
|
|
* @covers \MediaWiki\Extension\Scribunto\Engines\LuaSandbox\LuaSandboxEngine
|
|
* @covers \MediaWiki\Extension\Scribunto\Engines\LuaCommon\LuaInterpreter
|
|
* @covers \MediaWiki\Extension\Scribunto\Engines\LuaStandalone\LuaStandaloneInterpreter
|
|
* @covers \MediaWiki\Extension\Scribunto\Engines\LuaSandbox\LuaSandboxInterpreter
|
|
* @group Database
|
|
*/
|
|
class LuaCommonTest extends 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' => LuaCommonTestsLibrary::class,
|
|
'deferLoad' => true,
|
|
],
|
|
'CommonTestsFailLib' => [
|
|
'class' => 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 ( 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() );
|
|
}
|
|
}
|