MediaWiki REL1_34
CommonTest.php
Go to the documentation of this file.
1<?php
2
13 protected static $moduleName = 'CommonTests';
14
15 private static $allowedGlobals = [
16 // Functions
17 'assert',
18 'error',
19 'getfenv',
20 'getmetatable',
21 'ipairs',
22 'next',
23 'pairs',
24 'pcall',
25 'rawequal',
26 'rawget',
27 'rawset',
28 'require',
29 'select',
30 'setfenv',
31 'setmetatable',
32 'tonumber',
33 'tostring',
34 'type',
35 'unpack',
36 'xpcall',
37
38 // Packages
39 '_G',
40 'debug',
41 'math',
42 'mw',
43 'os',
44 'package',
45 'string',
46 'table',
47
48 // Misc
49 '_VERSION',
50 ];
51
52 protected function setUp() {
53 parent::setUp();
54
55 // Register libraries for self::testPHPLibrary()
56 $this->mergeMwGlobalArrayValue( 'wgHooks', [
57 'ScribuntoExternalLibraries' => [
58 function ( $engine, &$libs ) {
59 $libs += [
60 'CommonTestsLib' => [
61 'class' => 'Scribunto_LuaCommonTestsLibrary',
62 'deferLoad' => true,
63 ],
64 'CommonTestsFailLib' => [
65 'class' => 'Scribunto_LuaCommonTestsFailLibrary',
66 'deferLoad' => true,
67 ],
68 ];
69 }
70 ]
71 ] );
72
73 // Note this depends on every iteration of the data provider running with a clean parser
74 $this->getEngine()->getParser()->getOptions()->setExpensiveParserFunctionLimit( 10 );
75
76 // Some of the tests need this
77 $interpreter = $this->getEngine()->getInterpreter();
78 $interpreter->callFunction( $interpreter->loadString(
79 'mw.makeProtectedEnvFuncsForTest = mw.makeProtectedEnvFuncs', 'fortest'
80 ) );
81 }
82
83 protected function getTestModules() {
84 return parent::getTestModules() + [
85 'CommonTests' => __DIR__ . '/CommonTests.lua',
86 'CommonTests-data' => __DIR__ . '/CommonTests-data.lua',
87 'CommonTests-data-fail1' => __DIR__ . '/CommonTests-data-fail1.lua',
88 'CommonTests-data-fail2' => __DIR__ . '/CommonTests-data-fail2.lua',
89 'CommonTests-data-fail3' => __DIR__ . '/CommonTests-data-fail3.lua',
90 'CommonTests-data-fail4' => __DIR__ . '/CommonTests-data-fail4.lua',
91 'CommonTests-data-fail5' => __DIR__ . '/CommonTests-data-fail5.lua',
92 ];
93 }
94
95 public function testNoLeakedGlobals() {
96 $interpreter = $this->getEngine()->getInterpreter();
97
98 list( $actualGlobals ) = $interpreter->callFunction(
99 $interpreter->loadString(
100 'local t = {} for k in pairs( _G ) do t[#t+1] = k end return t',
101 'getglobals'
102 )
103 );
104
105 $leakedGlobals = array_diff( $actualGlobals, self::$allowedGlobals );
106 $this->assertEquals( 0, count( $leakedGlobals ),
107 'The following globals are leaked: ' . implode( ' ', $leakedGlobals )
108 );
109 }
110
111 public function testPHPLibrary() {
112 $engine = $this->getEngine();
113 $frame = $engine->getParser()->getPreprocessor()->newFrame();
114
115 $title = Title::makeTitle( NS_MODULE, 'TestInfoPassViaPHPLibrary' );
116 $this->extraModules[$title->getFullText()] = '
117 local p = {}
118
119 function p.test()
120 local lib = require( "CommonTestsLib" )
121 return table.concat( { lib.test() }, "; " )
122 end
123
124 function p.setVal( frame )
125 local lib = require( "CommonTestsLib" )
126 lib.val = frame.args[1]
127 lib.foobar.val = frame.args[1]
128 end
129
130 function p.getVal()
131 local lib = require( "CommonTestsLib" )
132 return tostring( lib.val ), tostring( lib.foobar.val )
133 end
134
135 function p.getSetVal( frame )
136 p.setVal( frame )
137 return p.getVal()
138 end
139
140 function p.checkPackage()
141 local ret = {}
142 ret[1] = package.loaded["CommonTestsLib"] == nil
143 require( "CommonTestsLib" )
144 ret[2] = package.loaded["CommonTestsLib"] ~= nil
145 return ret[1], ret[2]
146 end
147
148 function p.libSetVal( frame )
149 local lib = require( "CommonTestsLib" )
150 return lib.setVal( frame )
151 end
152
153 function p.libGetVal()
154 local lib = require( "CommonTestsLib" )
155 return lib.getVal()
156 end
157
158 return p
159 ';
160
161 # Test loading
162 $module = $engine->fetchModuleFromParser( $title );
163 $ret = $module->invoke( 'test', $frame->newChild() );
164 $this->assertSame( 'Test option; Test function', $ret,
165 'Library can be loaded and called' );
166
167 # Test package.loaded
168 $module = $engine->fetchModuleFromParser( $title );
169 $ret = $module->invoke( 'checkPackage', $frame->newChild() );
170 $this->assertSame( 'truetrue', $ret,
171 'package.loaded is right on the first call' );
172 $ret = $module->invoke( 'checkPackage', $frame->newChild() );
173 $this->assertSame( 'truetrue', $ret,
174 'package.loaded is right on the second call' );
175
176 # Test caching for require
177 $args = $engine->getParser()->getPreprocessor()->newPartNodeArray( [ 1 => 'cached' ] );
178 $ret = $module->invoke( 'getSetVal', $frame->newChild( $args ) );
179 $this->assertSame( 'cachedcached', $ret,
180 'same loaded table is returned by multiple require calls' );
181
182 # Test no data communication between invokes
183 $module = $engine->fetchModuleFromParser( $title );
184 $args = $engine->getParser()->getPreprocessor()->newPartNodeArray( [ 1 => 'fail' ] );
185 $module->invoke( 'setVal', $frame->newChild( $args ) );
186 $ret = $module->invoke( 'getVal', $frame->newChild() );
187 $this->assertSame( 'nilnope', $ret,
188 'same loaded table is not shared between invokes' );
189
190 # Test that the library isn't being recreated between invokes
191 $module = $engine->fetchModuleFromParser( $title );
192 $ret = $module->invoke( 'libGetVal', $frame->newChild() );
193 $this->assertSame( 'nil', $ret, 'sanity check' );
194 $args = $engine->getParser()->getPreprocessor()->newPartNodeArray( [ 1 => 'ok' ] );
195 $module->invoke( 'libSetVal', $frame->newChild( $args ) );
196
197 $module = $engine->fetchModuleFromParser( $title );
198 $ret = $module->invoke( 'libGetVal', $frame->newChild() );
199 $this->assertSame( 'ok', $ret,
200 'library is not recreated between invokes' );
201 }
202
203 public function testModuleStringExtend() {
204 $engine = $this->getEngine();
205 $interpreter = $engine->getInterpreter();
206
207 $interpreter->callFunction(
208 $interpreter->loadString( 'string.testModuleStringExtend = "ok"', 'extendstring' )
209 );
210 $ret = $interpreter->callFunction(
211 $interpreter->loadString( 'return ("").testModuleStringExtend', 'teststring1' )
212 );
213 $this->assertSame( [ 'ok' ], $ret, 'string can be extended' );
214
215 $this->extraModules['Module:testModuleStringExtend'] = '
216 return {
217 test = function() return ("").testModuleStringExtend end
218 }
219 ';
220 $module = $engine->fetchModuleFromParser(
221 Title::makeTitle( NS_MODULE, 'testModuleStringExtend' )
222 );
223 $ret = $interpreter->callFunction(
224 $engine->executeModule( $module->getInitChunk(), 'test', null )
225 );
226 $this->assertSame( [ 'ok' ], $ret, 'string extension can be used from module' );
227
228 $this->extraModules['Module:testModuleStringExtend2'] = '
229 return {
230 test = function()
231 string.testModuleStringExtend = "fail"
232 return ("").testModuleStringExtend
233 end
234 }
235 ';
236 $module = $engine->fetchModuleFromParser(
237 Title::makeTitle( NS_MODULE, 'testModuleStringExtend2' )
238 );
239 $ret = $interpreter->callFunction(
240 $engine->executeModule( $module->getInitChunk(), 'test', null )
241 );
242 $this->assertSame( [ 'ok' ], $ret, 'string extension cannot be modified from module' );
243 $ret = $interpreter->callFunction(
244 $interpreter->loadString( 'return string.testModuleStringExtend', 'teststring2' )
245 );
246 $this->assertSame( [ 'ok' ], $ret, 'string extension cannot be modified from module' );
247
248 $ret = $engine->runConsole( [
249 'prevQuestions' => [],
250 'question' => '=("").testModuleStringExtend',
251 'content' => 'return {}',
252 'title' => Title::makeTitle( NS_MODULE, 'dummy' ),
253 ] );
254 $this->assertSame( 'ok', $ret['return'], 'string extension can be used from console' );
255
256 $ret = $engine->runConsole( [
257 'prevQuestions' => [ 'string.fail = "fail"' ],
258 'question' => '=("").fail',
259 'content' => 'return {}',
260 'title' => Title::makeTitle( NS_MODULE, 'dummy' ),
261 ] );
262 $this->assertSame( 'nil', $ret['return'], 'string cannot be extended from console' );
263
264 $ret = $engine->runConsole( [
265 'prevQuestions' => [ 'string.testModuleStringExtend = "fail"' ],
266 'question' => '=("").testModuleStringExtend',
267 'content' => 'return {}',
268 'title' => Title::makeTitle( NS_MODULE, 'dummy' ),
269 ] );
270 $this->assertSame( 'ok', $ret['return'], 'string extension cannot be modified from console' );
271 $ret = $interpreter->callFunction(
272 $interpreter->loadString( 'return string.testModuleStringExtend', 'teststring3' )
273 );
274 $this->assertSame( [ 'ok' ], $ret, 'string extension cannot be modified from console' );
275
276 $interpreter->callFunction(
277 $interpreter->loadString( 'string.testModuleStringExtend = nil', 'unextendstring' )
278 );
279 }
280
281 public function testLoadDataLoadedOnce() {
282 $engine = $this->getEngine();
283 $interpreter = $engine->getInterpreter();
284 $frame = $engine->getParser()->getPreprocessor()->newFrame();
285
286 $loadcount = 0;
287 $interpreter->callFunction(
288 $interpreter->loadString( 'mw.markLoaded = ...', 'fortest' ),
289 $interpreter->wrapPHPFunction( function () use ( &$loadcount ) {
290 $loadcount++;
291 } )
292 );
293 $this->extraModules['Module:TestLoadDataLoadedOnce-data'] = '
294 mw.markLoaded()
295 return {}
296 ';
297 $this->extraModules['Module:TestLoadDataLoadedOnce'] = '
298 local data = mw.loadData( "Module:TestLoadDataLoadedOnce-data" )
299 return {
300 foo = function() end,
301 bar = function()
302 return tostring( package.loaded["Module:TestLoadDataLoadedOnce-data"] )
303 end,
304 }
305 ';
306
307 // Make sure data module isn't parsed twice. Simulate several {{#invoke:}}s
308 $title = Title::makeTitle( NS_MODULE, 'TestLoadDataLoadedOnce' );
309 for ( $i = 0; $i < 10; $i++ ) {
310 $module = $engine->fetchModuleFromParser( $title );
311 $module->invoke( 'foo', $frame->newChild() );
312 }
313 $this->assertSame( 1, $loadcount, 'data module was loaded more than once' );
314
315 // Make sure data module isn't in package.loaded
316 $this->assertSame( 'nil', $module->invoke( 'bar', $frame ),
317 'data module was stored in module\'s package.loaded'
318 );
319 $this->assertSame( [ 'nil' ],
320 $interpreter->callFunction( $interpreter->loadString(
321 'return tostring( package.loaded["Module:TestLoadDataLoadedOnce-data"] )', 'getLoaded'
322 ) ),
323 'data module was stored in top level\'s package.loaded'
324 );
325 }
326
327 public function testFrames() {
328 $engine = $this->getEngine();
329
330 $ret = $engine->runConsole( [
331 'prevQuestions' => [],
332 'question' => '=mw.getCurrentFrame()',
333 'content' => 'return {}',
334 'title' => Title::makeTitle( NS_MODULE, 'dummy' ),
335 ] );
336 $this->assertSame( 'table', $ret['return'], 'frames can be used in the console' );
337
338 $ret = $engine->runConsole( [
339 'prevQuestions' => [],
340 'question' => '=mw.getCurrentFrame():newChild{}',
341 'content' => 'return {}',
342 'title' => Title::makeTitle( NS_MODULE, 'dummy' ),
343 ] );
344 $this->assertSame( 'table', $ret['return'], 'child frames can be created' );
345
346 $ret = $engine->runConsole( [
347 'prevQuestions' => [
348 'f = mw.getCurrentFrame():newChild{ args = { "ok" } }',
349 'f2 = f:newChild{ args = {} }'
350 ],
351 'question' => '=f2:getParent().args[1], f2:getParent():getParent()',
352 'content' => 'return {}',
353 'title' => Title::makeTitle( NS_MODULE, 'dummy' ),
354 ] );
355 $this->assertSame( "ok\ttable", $ret['return'], 'child frames have correct parents' );
356 }
357
358 public function testCallParserFunction() {
359 $engine = $this->getEngine();
360 $parser = $engine->getParser();
361
362 $args = [
363 'prevQuestions' => [],
364 'content' => 'return {}',
365 'title' => Title::makeTitle( NS_MODULE, 'dummy' ),
366 ];
367
368 // Test argument calling conventions
369 $ret = $engine->runConsole( [
370 'question' => '=mw.getCurrentFrame():callParserFunction{
371 name = "urlencode", args = { "x x", "wiki" }
372 }',
373 ] + $args );
374 $this->assertSame( "x_x", $ret['return'],
375 'callParserFunction works for {{urlencode:x x|wiki}} (named args w/table)'
376 );
377
378 $ret = $engine->runConsole( [
379 'question' => '=mw.getCurrentFrame():callParserFunction{
380 name = "urlencode", args = "x x"
381 }',
382 ] + $args );
383 $this->assertSame( "x+x", $ret['return'],
384 'callParserFunction works for {{urlencode:x x}} (named args w/scalar)'
385 );
386
387 $ret = $engine->runConsole( [
388 'question' => '=mw.getCurrentFrame():callParserFunction( "urlencode", { "x x", "wiki" } )',
389 ] + $args );
390 $this->assertSame( "x_x", $ret['return'],
391 'callParserFunction works for {{urlencode:x x|wiki}} (positional args w/table)'
392 );
393
394 $ret = $engine->runConsole( [
395 'question' => '=mw.getCurrentFrame():callParserFunction( "urlencode", "x x", "wiki" )',
396 ] + $args );
397 $this->assertSame( "x_x", $ret['return'],
398 'callParserFunction works for {{urlencode:x x|wiki}} (positional args w/scalars)'
399 );
400
401 $ret = $engine->runConsole( [
402 'question' => '=mw.getCurrentFrame():callParserFunction{
403 name = "urlencode:x x", args = { "wiki" }
404 }',
405 ] + $args );
406 $this->assertSame( "x_x", $ret['return'],
407 'callParserFunction works for {{urlencode:x x|wiki}} (colon in name, named args w/table)'
408 );
409
410 $ret = $engine->runConsole( [
411 'question' => '=mw.getCurrentFrame():callParserFunction{
412 name = "urlencode:x x", args = "wiki"
413 }',
414 ] + $args );
415 $this->assertSame( "x_x", $ret['return'],
416 'callParserFunction works for {{urlencode:x x|wiki}} (colon in name, named args w/scalar)'
417 );
418
419 $ret = $engine->runConsole( [
420 'question' => '=mw.getCurrentFrame():callParserFunction( "urlencode:x x", { "wiki" } )',
421 ] + $args );
422 $this->assertSame( "x_x", $ret['return'],
423 'callParserFunction works for {{urlencode:x x|wiki}} (colon in name, positional args w/table)'
424 );
425
426 $ret = $engine->runConsole( [
427 'question' => '=mw.getCurrentFrame():callParserFunction( "urlencode:x x", "wiki" )',
428 ] + $args );
429 $this->assertSame( "x_x", $ret['return'],
430 'callParserFunction works for {{urlencode:x x|wiki}} (colon in name, positional args w/scalars)'
431 );
432
433 // Test named args to the parser function
434 $ret = $engine->runConsole( [
435 'question' => '=mw.getCurrentFrame():callParserFunction( "#tag:pre",
436 { "foo", style = "margin-left: 1.6em" }
437 )',
438 ] + $args );
439 $this->assertSame(
440 '<pre style="margin-left: 1.6em">foo</pre>',
441 $parser->mStripState->unstripBoth( $ret['return'] ),
442 'callParserFunction works for {{#tag:pre|foo|style=margin-left: 1.6em}}'
443 );
444
445 // Test extensionTag
446 $ret = $engine->runConsole( [
447 'question' => '=mw.getCurrentFrame():extensionTag( "pre", "foo",
448 { style = "margin-left: 1.6em" }
449 )',
450 ] + $args );
451 $this->assertSame(
452 '<pre style="margin-left: 1.6em">foo</pre>',
453 $parser->mStripState->unstripBoth( $ret['return'] ),
454 'extensionTag works for {{#tag:pre|foo|style=margin-left: 1.6em}}'
455 );
456
457 $ret = $engine->runConsole( [
458 'question' => '=mw.getCurrentFrame():extensionTag{ name = "pre", content = "foo",
459 args = { style = "margin-left: 1.6em" }
460 }',
461 ] + $args );
462 $this->assertSame(
463 '<pre style="margin-left: 1.6em">foo</pre>',
464 $parser->mStripState->unstripBoth( $ret['return'] ),
465 'extensionTag works for {{#tag:pre|foo|style=margin-left: 1.6em}}'
466 );
467
468 // Test calling a non-existent function
469 try {
470 $ret = $engine->runConsole( [
471 'question' => '=mw.getCurrentFrame():callParserFunction{
472 name = "thisDoesNotExist", args = { "" }
473 }',
474 ] + $args );
475 $this->fail( "Expected LuaError not thrown for nonexistent parser function" );
476 } catch ( Scribunto_LuaError $err ) {
477 $this->assertSame(
478 'Lua error: callParserFunction: function "thisDoesNotExist" was not found.',
479 $err->getMessage(),
480 'callParserFunction correctly errors for nonexistent function'
481 );
482 }
483 }
484
485 public function testBug62291() {
486 $engine = $this->getEngine();
487 $frame = $engine->getParser()->getPreprocessor()->newFrame();
488
489 $this->extraModules['Module:Bug62291'] = '
490 local p = {}
491 function p.foo()
492 return table.concat( {
493 math.random(), math.random(), math.random(), math.random(), math.random()
494 }, ", " )
495 end
496 function p.bar()
497 local t = {}
498 t[1] = p.foo()
499 t[2] = mw.getCurrentFrame():preprocess( "{{#invoke:Bug62291|bar2}}" )
500 t[3] = p.foo()
501 return table.concat( t, "; " )
502 end
503 function p.bar2()
504 return "bar2 called"
505 end
506 return p
507 ';
508
509 $title = Title::makeTitle( NS_MODULE, 'Bug62291' );
510 $module = $engine->fetchModuleFromParser( $title );
511
512 // Make sure multiple invokes return the same text
513 $r1 = $module->invoke( 'foo', $frame->newChild() );
514 $r2 = $module->invoke( 'foo', $frame->newChild() );
515 $this->assertSame( $r1, $r2, 'Multiple invokes returned different sets of random numbers' );
516
517 // Make sure a recursive invoke doesn't reset the PRNG
518 $r1 = $module->invoke( 'bar', $frame->newChild() );
519 $r = explode( '; ', $r1 );
520 $this->assertNotSame( $r[0], $r[2], 'Recursive invoke reset PRNG' );
521 $this->assertSame( 'bar2 called', $r[1], 'Sanity check failed' );
522
523 // But a second invoke does
524 $r2 = $module->invoke( 'bar', $frame->newChild() );
525 $this->assertSame( $r1, $r2,
526 'Multiple invokes with recursive invoke returned different sets of random numbers' );
527 }
528
529 public function testOsDateTimeTTLs() {
530 $engine = $this->getEngine();
531 $pp = $engine->getParser()->getPreprocessor();
532
533 $this->extraModules['Module:DateTime'] = '
534 local p = {}
535 function p.day()
536 return os.date( "%d" )
537 end
538 function p.AMPM()
539 return os.date( "%p" )
540 end
541 function p.hour()
542 return os.date( "%H" )
543 end
544 function p.minute()
545 return os.date( "%M" )
546 end
547 function p.second()
548 return os.date( "%S" )
549 end
550 function p.table()
551 return os.date( "*t" )
552 end
553 function p.tablesec()
554 return os.date( "*t" ).sec
555 end
556 function p.time()
557 return os.time()
558 end
559 function p.specificDateAndTime()
560 return os.date("%S", os.time{year = 2013, month = 1, day = 1})
561 end
562 return p
563 ';
564
565 $title = Title::makeTitle( NS_MODULE, 'DateTime' );
566 $module = $engine->fetchModuleFromParser( $title );
567
568 $frame = $pp->newFrame();
569 $module->invoke( 'day', $frame );
570 $this->assertNotNull( $frame->getTTL(), 'TTL must be set when day is requested' );
571 $this->assertLessThanOrEqual( 86400, $frame->getTTL(),
572 'TTL must not exceed 1 day when day is requested' );
573
574 $frame = $pp->newFrame();
575 $module->invoke( 'AMPM', $frame );
576 $this->assertNotNull( $frame->getTTL(), 'TTL must be set when AM/PM is requested' );
577 $this->assertLessThanOrEqual( 43200, $frame->getTTL(),
578 'TTL must not exceed 12 hours when AM/PM is requested' );
579
580 $frame = $pp->newFrame();
581 $module->invoke( 'hour', $frame );
582 $this->assertNotNull( $frame->getTTL(), 'TTL must be set when hour is requested' );
583 $this->assertLessThanOrEqual( 3600, $frame->getTTL(),
584 'TTL must not exceed 1 hour when hours are requested' );
585
586 $frame = $pp->newFrame();
587 $module->invoke( 'minute', $frame );
588 $this->assertNotNull( $frame->getTTL(), 'TTL must be set when minutes are requested' );
589 $this->assertLessThanOrEqual( 60, $frame->getTTL(),
590 'TTL must not exceed 1 minute when minutes are requested' );
591
592 $frame = $pp->newFrame();
593 $module->invoke( 'second', $frame );
594 $this->assertEquals( 1, $frame->getTTL(),
595 'TTL must be equal to 1 second when seconds are requested' );
596
597 $frame = $pp->newFrame();
598 $module->invoke( 'table', $frame );
599 $this->assertNull( $frame->getTTL(),
600 'TTL must not be set when os.date( "*t" ) is called but no values are looked at' );
601
602 $frame = $pp->newFrame();
603 $module->invoke( 'tablesec', $frame );
604 $this->assertEquals( 1, $frame->getTTL(),
605 'TTL must be equal to 1 second when seconds are requested from a table' );
606
607 $frame = $pp->newFrame();
608 $module->invoke( 'time', $frame );
609 $this->assertEquals( 1, $frame->getTTL(),
610 'TTL must be equal to 1 second when os.time() is called' );
611
612 $frame = $pp->newFrame();
613 $module->invoke( 'specificDateAndTime', $frame );
614 $this->assertNull( $frame->getTTL(),
615 'TTL must not be set when os.date() or os.time() are called with a specific time' );
616 }
617
621 public function testVolatileCaching( $func ) {
622 $engine = $this->getEngine();
623 $parser = $engine->getParser();
624 $pp = $parser->getPreprocessor();
625
626 $count = 0;
627 $parser->setHook( 'scribuntocount', function ( $str, $argv, $parser, $frame ) use ( &$count ) {
628 $frame->setVolatile();
629 return ++$count;
630 } );
631
632 $this->extraModules['Template:ScribuntoTestVolatileCaching'] = '<scribuntocount/>';
633 $this->extraModules['Module:TestVolatileCaching'] = '
634 return {
635 preprocess = function ( frame )
636 return frame:preprocess( "<scribuntocount/>" )
637 end,
638 extensionTag = function ( frame )
639 return frame:extensionTag( "scribuntocount" )
640 end,
641 expandTemplate = function ( frame )
642 return frame:expandTemplate{ title = "ScribuntoTestVolatileCaching" }
643 end,
644 }
645 ';
646
647 $frame = $pp->newFrame();
648 $count = 0;
649 $wikitext = "{{#invoke:TestVolatileCaching|$func}}";
650 $text = $frame->expand( $pp->preprocessToObj( "$wikitext $wikitext" ) );
651 $text = $parser->mStripState->unstripBoth( $text );
652 $this->assertTrue( $frame->isVolatile(), "Frame is marked volatile" );
653 $this->assertEquals( '1 2', $text, "Volatile wikitext was not cached" );
654 }
655
656 public function provideVolatileCaching() {
657 return [
658 [ 'preprocess' ],
659 [ 'extensionTag' ],
660 [ 'expandTemplate' ],
661 ];
662 }
663
665 $engine = $this->getEngine();
666 $parser = $engine->getParser();
667 $pp = $parser->getPreprocessor();
668
669 $this->extraModules['Module:Bug65687'] = '
670 return {
671 test = function ( frame )
672 return mw.loadData( "Module:Bug65687-LD" )[1]
673 end
674 }
675 ';
676 $this->extraModules['Module:Bug65687-LD'] = 'return { mw.getCurrentFrame().args[1] or "ok" }';
677
678 $frame = $pp->newFrame();
679 $text = $frame->expand( $pp->preprocessToObj( "{{#invoke:Bug65687|test|foo}}" ) );
680 $text = $parser->mStripState->unstripBoth( $text );
681 $this->assertEquals( 'ok', $text, 'mw.loadData allowed access to frame args' );
682 }
683
685 $engine = $this->getEngine();
686 $parser = $engine->getParser();
687 $pp = $parser->getPreprocessor();
688
689 $this->extraModules['Module:Bug67498-directly'] = '
690 local f = mw.getCurrentFrame()
691 local f2 = f and f.args[1] or "<none>"
692
693 return {
694 test = function ( frame )
695 return ( f and f.args[1] or "<none>" ) .. " " .. f2
696 end
697 }
698 ';
699 $this->extraModules['Module:Bug67498-statically'] = '
700 local M = require( "Module:Bug67498-directly" )
701 return {
702 test = function ( frame )
703 return M.test( frame )
704 end
705 }
706 ';
707 $this->extraModules['Module:Bug67498-dynamically'] = '
708 return {
709 test = function ( frame )
710 local M = require( "Module:Bug67498-directly" )
711 return M.test( frame )
712 end
713 }
714 ';
715
716 foreach ( [ 'directly', 'statically', 'dynamically' ] as $how ) {
717 $frame = $pp->newFrame();
718 $text = $frame->expand( $pp->preprocessToObj(
719 "{{#invoke:Bug67498-$how|test|foo}} -- {{#invoke:Bug67498-$how|test|bar}}"
720 ) );
721 $text = $parser->mStripState->unstripBoth( $text );
722 $text = explode( ' -- ', $text );
723 $this->assertEquals( 'foo foo', $text[0],
724 "mw.getCurrentFrame() failed from a module loaded $how"
725 );
726 $this->assertEquals( 'bar bar', $text[1],
727 "mw.getCurrentFrame() cached the frame from a module loaded $how"
728 );
729 }
730 }
731
732 public function testNonUtf8Errors() {
733 $engine = $this->getEngine();
734 $parser = $engine->getParser();
735
736 $this->extraModules['Module:T208689'] = '
737 local p = {}
738
739 p["foo\255bar"] = function ()
740 error( "error\255bar" )
741 end
742
743 p.foo = function ()
744 p["foo\255bar"]()
745 end
746
747 return p
748 ';
749
750 // As via the API
751 try {
752 $engine->runConsole( [
753 'prevQuestions' => [],
754 'question' => 'p.foo()',
755 'title' => Title::newFromText( 'Module:T208689' ),
756 'content' => $this->extraModules['Module:T208689'],
757 ] );
758 $this->fail( 'Expected exception not thrown' );
759 } catch ( ScribuntoException $e ) {
760 $this->assertTrue( mb_check_encoding( $e->getMessage(), 'UTF-8' ), 'Message is UTF-8' );
761 $this->assertTrue( mb_check_encoding( $e->getScriptTraceHtml(), 'UTF-8' ), 'Message is UTF-8' );
762 }
763
764 // Via the parser
765 $text = $parser->recursiveTagParseFully( '{{#invoke:T208689|foo}}' );
766 $this->assertTrue( mb_check_encoding( $text, 'UTF-8' ), 'Parser output is UTF-8' );
767 $vars = $parser->getOutput()->getJsConfigVars();
768 $this->assertArrayHasKey( 'ScribuntoErrors', $vars );
769 foreach ( $vars['ScribuntoErrors'] as $err ) {
770 $this->assertTrue( mb_check_encoding( $err, 'UTF-8' ), 'JS config vars are UTF-8' );
771 }
772 }
773}
774
776 public function register() {
777 $lib = [
778 'test' => [ $this, 'test' ],
779 ];
780 $opts = [
781 'test' => 'Test option',
782 ];
783
784 return $this->getEngine()->registerInterface( __DIR__ . '/CommonTests-lib.lua', $lib, $opts );
785 }
786
787 public function test() {
788 return [ 'Test function' ];
789 }
790}
791
793 public function __construct() {
794 throw new MWException( 'deferLoad library that is never required was loaded anyway' );
795 }
796
797 public function register() {
798 }
799}
const NS_MODULE
if( $line===false) $args
Definition cdb.php:64
MediaWiki exception.
An exception class which represents an error in the script.
Definition Common.php:136
getScriptTraceHtml( $options=[])
Get the backtrace as HTML, or false if there is none available.
Definition Common.php:206
@covers ScribuntoEngineBase @covers Scribunto_LuaEngine @covers Scribunto_LuaStandaloneEngine @covers...
testVolatileCaching( $func)
@dataProvider provideVolatileCaching
This is the subclass for Lua library tests.
This class provides some basic services that Lua libraries will probably need.
getEngine()
Get the engine.