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