MediaWiki  1.34.0
LuaInterpreterTest.php
Go to the documentation of this file.
1 <?php
2 
3 abstract class Scribunto_LuaInterpreterTest extends PHPUnit\Framework\TestCase {
4  use MediaWikiCoversValidator;
5  use PHPUnit4And6Compat;
6 
10  abstract protected function newInterpreter( $opts = [] );
11 
12  protected function setUp() {
13  parent::setUp();
14  try {
15  $this->newInterpreter();
17  $this->markTestSkipped( "interpreter not available" );
18  }
19  }
20 
21  protected function getBusyLoop( $interpreter ) {
22  $chunk = $interpreter->loadString( '
23  local args = {...}
24  local x, i
25  local s = string.rep("x", 1000000)
26  local n = args[1]
27  local e = args[2] and os.clock() + args[2] or nil
28  for i = 1, n do
29  x = x or string.find(s, "y", 1, true)
30  if e and os.clock() >= e then break end
31  end',
32  'busy' );
33  return $chunk;
34  }
35 
37  public function testRoundtrip( /*...*/ ) {
38  $args = func_get_args();
39  $args = $this->normalizeOrder( $args );
40  $interpreter = $this->newInterpreter();
41  $passthru = $interpreter->loadString( 'return ...', 'passthru' );
42  $ret = $interpreter->callFunction( $passthru, ...$args );
43  $ret = $this->normalizeOrder( $ret );
44  $this->assertSame( $args, $ret );
45  }
46 
48  public function testDoubleRoundtrip( /* ... */ ) {
49  $args = func_get_args();
50  $args = $this->normalizeOrder( $args );
51 
52  $interpreter = $this->newInterpreter();
53  $interpreter->registerLibrary( 'test',
54  [ 'passthru' => [ $this, 'passthru' ] ] );
55  $doublePassthru = $interpreter->loadString(
56  'return test.passthru(...)', 'doublePassthru' );
57 
58  $finalArgs = $args;
59  array_unshift( $finalArgs, $doublePassthru );
60  $ret = $interpreter->callFunction( ...$finalArgs );
61  $ret = $this->normalizeOrder( $ret );
62  $this->assertSame( $args, $ret );
63  }
64 
69  public function testRoundtripNAN() {
70  $interpreter = $this->newInterpreter();
71 
72  $passthru = $interpreter->loadString( 'return ...', 'passthru' );
73  $ret = $interpreter->callFunction( $passthru, NAN );
74  $this->assertTrue( is_nan( $ret[0] ), 'NaN was not passed through' );
75 
76  $interpreter->registerLibrary( 'test',
77  [ 'passthru' => [ $this, 'passthru' ] ] );
78  $doublePassthru = $interpreter->loadString(
79  'return test.passthru(...)', 'doublePassthru' );
80  $ret = $interpreter->callFunction( $doublePassthru, NAN );
81  $this->assertTrue( is_nan( $ret[0] ), 'NaN was not double passed through' );
82  }
83 
84  private function normalizeOrder( $a ) {
85  ksort( $a );
86  foreach ( $a as &$value ) {
87  if ( is_array( $value ) ) {
88  $value = $this->normalizeOrder( $value );
89  }
90  }
91  return $a;
92  }
93 
94  public function passthru( /* ... */ ) {
95  $args = func_get_args();
96  return $args;
97  }
98 
99  public function provideRoundtrip() {
100  return [
101  [ 1 ],
102  [ true ],
103  [ false ],
104  [ 'hello' ],
105  [ implode( '', array_map( 'chr', range( 0, 255 ) ) ) ],
106  [ 1, 2, 3 ],
107  [ [] ],
108  [ [ 0 => 'foo', 1 => 'bar' ] ],
109  [ [ 1 => 'foo', 2 => 'bar' ] ],
110  [ [ 'x' => 'foo', 'y' => 'bar', 'z' => [] ] ],
111  [ INF ],
112  [ -INF ],
113  [ 'ok', null, 'ok' ],
114  [ null, 'ok' ],
115  [ 'ok', null ],
116  [ null ],
117  ];
118  }
119 
120  public function testTimeLimit() {
121  if ( php_uname( 's' ) === 'Darwin' ) {
122  $this->markTestSkipped( "Darwin is lacking POSIX timer, skipping CPU time limiting test." );
123  }
124 
125  $interpreter = $this->newInterpreter( [ 'cpuLimit' => 1 ] );
126  $chunk = $this->getBusyLoop( $interpreter );
127  try {
128  $interpreter->callFunction(
129  $chunk,
130  1e9, // Arbitrary large quantity of work for the loop
131  2 // Early termination condition: 1 second CPU limit plus 1 second "fudge factor"
132  );
133  $this->fail( "Expected ScribuntoException was not thrown" );
134  } catch ( ScribuntoException $ex ) {
135  $this->assertSame( 'scribunto-common-timeout', $ex->messageName );
136  }
137  }
138 
139  public function testTestMemoryLimit() {
140  $interpreter = $this->newInterpreter( [ 'memoryLimit' => 20 * 1e6 ] );
141  $chunk = $interpreter->loadString( '
142  t = {}
143  for i = 1, 10 do
144  t[#t + 1] = string.rep("x" .. i, 1000000)
145  end
146  ',
147  'memoryLimit' );
148  try {
149  $interpreter->callFunction( $chunk );
150  $this->fail( "Expected ScribuntoException was not thrown" );
151  } catch ( ScribuntoException $ex ) {
152  $this->assertSame( 'scribunto-lua-error', $ex->messageName );
153  $this->assertSame( 'not enough memory', $ex->messageArgs[1] );
154  }
155  }
156 
157  public function testWrapPHPFunction() {
158  $interpreter = $this->newInterpreter();
159  $func = $interpreter->wrapPhpFunction( function ( $n ) {
160  return [ 42, $n ];
161  } );
162  $res = $interpreter->callFunction( $func, 'From PHP' );
163  $this->assertEquals( [ 42, 'From PHP' ], $res );
164 
165  $chunk = $interpreter->loadString( '
166  f = ...
167  return f( "From Lua" )
168  ',
169  'wrappedPhpFunction' );
170  $res = $interpreter->callFunction( $chunk, $func );
171  $this->assertEquals( [ 42, 'From Lua' ], $res );
172  }
173 
175  $interpreter = $this->newInterpreter();
176  $test1Called = false;
177  $test2Called = false;
178 
179  // Like a first call to Scribunto_LuaEngine::registerInterface()
180  $interpreter->registerLibrary( 'mw_interface', [
181  'foo' => function ( $v ) use ( &$test1Called ) {
182  $test1Called = $v;
183  },
184  ] );
185  $interpreter->callFunction(
186  $interpreter->loadString( 'test1 = mw_interface; mw_interface = nil', 'test' )
187  );
188  // Like a second call to Scribunto_LuaEngine::registerInterface()
189  $interpreter->registerLibrary( 'mw_interface', [
190  'foo' => function ( $v ) use ( &$test2Called ) {
191  $test2Called = $v;
192  },
193  ] );
194  $interpreter->callFunction(
195  $interpreter->loadString( 'test2 = mw_interface; mw_interface = nil', 'test' )
196  );
197  // Call both of the interfaces registered above.
198  $interpreter->callFunction(
199  $interpreter->loadString( 'test1.foo( "first" ); test2.foo( "second" )', 'test' )
200  );
201  $this->assertSame( 'first', $test1Called, 'test1.foo was called with "first"' );
202  $this->assertSame( 'second', $test2Called, 'test2.foo was called with "second"' );
203  }
204 
205 }
true
return true
Definition: router.php:92
Scribunto_LuaInterpreterTest\provideRoundtrip
provideRoundtrip()
Definition: LuaInterpreterTest.php:99
Scribunto_LuaInterpreterTest\normalizeOrder
normalizeOrder( $a)
Definition: LuaInterpreterTest.php:84
$res
$res
Definition: testCompression.php:52
Scribunto_LuaInterpreterTest\testRoundtrip
testRoundtrip()
@dataProvider provideRoundtrip
Definition: LuaInterpreterTest.php:37
Scribunto_LuaInterpreterNotFoundError
Definition: LuaInterpreter.php:64
Scribunto_LuaInterpreterTest\testWrapPHPFunction
testWrapPHPFunction()
Definition: LuaInterpreterTest.php:157
Scribunto_LuaInterpreterTest\testTimeLimit
testTimeLimit()
Definition: LuaInterpreterTest.php:120
Scribunto_LuaInterpreterTest
Definition: LuaInterpreterTest.php:3
Scribunto_LuaInterpreterTest\setUp
setUp()
Definition: LuaInterpreterTest.php:12
Scribunto_LuaInterpreterTest\passthru
passthru()
Definition: LuaInterpreterTest.php:94
Scribunto_LuaInterpreterTest\newInterpreter
newInterpreter( $opts=[])
ScribuntoException
An exception class which represents an error in the script.
Definition: Common.php:136
$args
if( $line===false) $args
Definition: cdb.php:64
Scribunto_LuaInterpreterTest\testDoubleRoundtrip
testDoubleRoundtrip()
@dataProvider provideRoundtrip
Definition: LuaInterpreterTest.php:48
Scribunto_LuaInterpreterTest\testRoundtripNAN
testRoundtripNAN()
This cannot be done in testRoundtrip and testDoubleRoundtrip, because assertSame( NAN,...
Definition: LuaInterpreterTest.php:69
Scribunto_LuaInterpreterTest\testRegisterInterfaceWithSameName
testRegisterInterfaceWithSameName()
Definition: LuaInterpreterTest.php:174
Scribunto_LuaInterpreterTest\testTestMemoryLimit
testTestMemoryLimit()
Definition: LuaInterpreterTest.php:139
Scribunto_LuaInterpreterTest\getBusyLoop
getBusyLoop( $interpreter)
Definition: LuaInterpreterTest.php:21