mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Scribunto
synced 2024-12-02 20:06:14 +00:00
6b4cfd5b94
In Lua, a table entry with a nil value is the same as a table entry that doesn't exist. So when serializing for transfer to PHP, these keys will be skipped. For a table as an associative array this isn't much of a problem, but for a table as a list it means we have missing indexes. Some of Lua's functions for handling "lists" (i.e. tables with numeric keys) also have a problem when the list contains nils. To work around these issues when passing argument lists and return value lists, pass the number of elements along with the sparse list. On the PHP end we can use this to fill in the missing nulls, and on the Lua end we can pass this count to unpack() to avoid the problems on the Lua side. Change-Id: I858e3905a06e377693301da2b8bc534808f00e3e
166 lines
4.6 KiB
PHP
166 lines
4.6 KiB
PHP
<?php
|
|
|
|
abstract class Scribunto_LuaInterpreterTest extends MediaWikiTestCase {
|
|
abstract function newInterpreter( $opts = array() );
|
|
|
|
function setUp() {
|
|
parent::setUp();
|
|
try {
|
|
$this->newInterpreter();
|
|
} catch ( Scribunto_LuaInterpreterNotFoundError $e ) {
|
|
$this->markTestSkipped( "interpreter not available" );
|
|
}
|
|
}
|
|
|
|
function getBusyLoop( $interpreter ) {
|
|
$chunk = $interpreter->loadString( '
|
|
local args = {...}
|
|
local x, i
|
|
local s = string.rep("x", 1000000)
|
|
local n = args[1]
|
|
for i = 1, n do
|
|
x = x or string.find(s, "y", 1, true)
|
|
end',
|
|
'busy' );
|
|
return $chunk;
|
|
}
|
|
|
|
function getPassthru( $interpreter ) {
|
|
return $interpreter->loadString( 'return ...', 'passthru' );
|
|
}
|
|
|
|
/** @dataProvider provideRoundtrip */
|
|
function testRoundtrip( /*...*/ ) {
|
|
$args = func_get_args();
|
|
$args = $this->normalizeOrder( $args );
|
|
$interpreter = $this->newInterpreter();
|
|
$passthru = $interpreter->loadString( 'return ...', 'passthru' );
|
|
$finalArgs = $args;
|
|
array_unshift( $finalArgs, $passthru );
|
|
$ret = call_user_func_array( array( $interpreter, 'callFunction' ), $finalArgs );
|
|
$ret = $this->normalizeOrder( $ret );
|
|
$this->assertSame( $args, $ret );
|
|
}
|
|
|
|
/** @dataProvider provideRoundtrip */
|
|
function testDoubleRoundtrip( /* ... */ ) {
|
|
$args = func_get_args();
|
|
$args = $this->normalizeOrder( $args );
|
|
|
|
$interpreter = $this->newInterpreter();
|
|
$interpreter->registerLibrary( 'test',
|
|
array( 'passthru' => array( $this, 'passthru' ) ) );
|
|
$doublePassthru = $interpreter->loadString(
|
|
'return test.passthru(...)', 'doublePassthru' );
|
|
|
|
$finalArgs = $args;
|
|
array_unshift( $finalArgs, $doublePassthru );
|
|
$ret = call_user_func_array( array( $interpreter, 'callFunction' ), $finalArgs );
|
|
$ret = $this->normalizeOrder( $ret );
|
|
$this->assertSame( $args, $ret );
|
|
}
|
|
|
|
/**
|
|
* This cannot be done in testRoundtrip and testDoubleRoundtrip, because
|
|
* assertSame( NAN, NAN ) returns false.
|
|
*/
|
|
function testRoundtripNAN() {
|
|
$interpreter = $this->newInterpreter();
|
|
|
|
$passthru = $interpreter->loadString( 'return ...', 'passthru' );
|
|
$ret = $interpreter->callFunction( $passthru, NAN );
|
|
$this->assertEquals( array( NAN ), $ret );
|
|
|
|
$interpreter->registerLibrary( 'test',
|
|
array( 'passthru' => array( $this, 'passthru' ) ) );
|
|
$doublePassthru = $interpreter->loadString(
|
|
'return test.passthru(...)', 'doublePassthru' );
|
|
$ret = $interpreter->callFunction( $doublePassthru, NAN );
|
|
$this->assertEquals( array( NAN ), $ret );
|
|
}
|
|
|
|
function normalizeOrder( $a ) {
|
|
ksort( $a );
|
|
foreach ( $a as &$value ) {
|
|
if ( is_array( $value ) ) {
|
|
$value = $this->normalizeOrder( $value );
|
|
}
|
|
}
|
|
return $a;
|
|
}
|
|
|
|
function passthru( /* ... */ ) {
|
|
$args = func_get_args();
|
|
return $args;
|
|
}
|
|
|
|
function provideRoundtrip() {
|
|
return array(
|
|
array( 1 ),
|
|
array( true ),
|
|
array( false ),
|
|
array( 'hello' ),
|
|
array( implode( '', array_map( 'chr', range( 0, 255 ) ) ) ),
|
|
array( 1, 2, 3 ),
|
|
array( array() ),
|
|
array( array( 0 => 'foo', 1 => 'bar' ) ),
|
|
array( array( 1 => 'foo', 2 => 'bar' ) ),
|
|
array( array( 'x' => 'foo', 'y' => 'bar', 'z' => array() ) ),
|
|
array( INF ),
|
|
array( -INF ),
|
|
array( 'ok', null, 'ok' ),
|
|
array( null, 'ok' ),
|
|
array( 'ok', null ),
|
|
array( null ),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @expectedException ScribuntoException
|
|
* @expectedExceptionMessage The time allocated for running scripts has expired.
|
|
*/
|
|
function testTimeLimit() {
|
|
if( php_uname( 's' ) === 'Darwin' ) {
|
|
$this->markTestSkipped( "Darwin is lacking POSIX timer, skipping CPU time limiting test." );
|
|
}
|
|
|
|
$interpreter = $this->newInterpreter( array( 'cpuLimit' => 1 ) );
|
|
$chunk = $this->getBusyLoop( $interpreter );
|
|
$interpreter->callFunction( $chunk, 1e9 );
|
|
}
|
|
|
|
/**
|
|
* @expectedException ScribuntoException
|
|
* @expectedExceptionMessage Lua error: not enough memory
|
|
*/
|
|
function testTestMemoryLimit() {
|
|
$interpreter = $this->newInterpreter( array( 'memoryLimit' => 20 * 1e6 ) );
|
|
$chunk = $interpreter->loadString( '
|
|
t = {}
|
|
for i = 1, 10 do
|
|
t[#t + 1] = string.rep("x" .. i, 1000000)
|
|
end
|
|
',
|
|
'memoryLimit' );
|
|
$interpreter->callFunction( $chunk );
|
|
}
|
|
|
|
function testWrapPHPFunction() {
|
|
$interpreter = $this->newInterpreter();
|
|
$func = $interpreter->wrapPhpFunction( function ( $n ) {
|
|
return array( 42, $n );
|
|
} );
|
|
$res = $interpreter->callFunction( $func, 'From PHP' );
|
|
$this->assertEquals( array( 42, 'From PHP' ), $res );
|
|
|
|
$chunk = $interpreter->loadString( '
|
|
f = ...
|
|
return f( "From Lua" )
|
|
',
|
|
'wrappedPhpFunction' );
|
|
$res = $interpreter->callFunction( $chunk, $func );
|
|
$this->assertEquals( array( 42, 'From Lua' ), $res );
|
|
}
|
|
}
|
|
|