MediaWiki
REL1_22
|
00001 <?php 00002 00012 class NewParserTest extends MediaWikiTestCase { 00013 static protected $articles = array(); // Array of test articles defined by the tests 00014 /* The data provider is run on a different instance than the test, so it must be static 00015 * When running tests from several files, all tests will see all articles. 00016 */ 00017 static protected $backendToUse; 00018 00019 public $keepUploads = false; 00020 public $runDisabled = false; 00021 public $runParsoid = false; 00022 public $regex = ''; 00023 public $showProgress = true; 00024 public $savedWeirdGlobals = array(); 00025 public $savedGlobals = array(); 00026 public $hooks = array(); 00027 public $functionHooks = array(); 00028 00029 //Fuzz test 00030 public $maxFuzzTestLength = 300; 00031 public $fuzzSeed = 0; 00032 public $memoryLimit = 50; 00033 00034 protected $file = false; 00035 00036 public static function setUpBeforeClass() { 00037 // Inject ParserTest well-known interwikis 00038 ParserTest::setupInterwikis(); 00039 } 00040 00041 protected function setUp() { 00042 global $wgNamespaceAliases, $wgContLang; 00043 global $wgHooks, $IP; 00044 00045 parent::setUp(); 00046 00047 //Setup CLI arguments 00048 if ( $this->getCliArg( 'regex=' ) ) { 00049 $this->regex = $this->getCliArg( 'regex=' ); 00050 } else { 00051 # Matches anything 00052 $this->regex = ''; 00053 } 00054 00055 $this->keepUploads = $this->getCliArg( 'keep-uploads' ); 00056 00057 $tmpGlobals = array(); 00058 00059 $tmpGlobals['wgLanguageCode'] = 'en'; 00060 $tmpGlobals['wgContLang'] = Language::factory( 'en' ); 00061 $tmpGlobals['wgSitename'] = 'MediaWiki'; 00062 $tmpGlobals['wgServer'] = 'http://example.org'; 00063 $tmpGlobals['wgScript'] = '/index.php'; 00064 $tmpGlobals['wgScriptPath'] = '/'; 00065 $tmpGlobals['wgArticlePath'] = '/wiki/$1'; 00066 $tmpGlobals['wgActionPaths'] = array(); 00067 $tmpGlobals['wgVariantArticlePath'] = false; 00068 $tmpGlobals['wgExtensionAssetsPath'] = '/extensions'; 00069 $tmpGlobals['wgStylePath'] = '/skins'; 00070 $tmpGlobals['wgEnableUploads'] = true; 00071 $tmpGlobals['wgThumbnailScriptPath'] = false; 00072 $tmpGlobals['wgLocalFileRepo'] = array( 00073 'class' => 'LocalRepo', 00074 'name' => 'local', 00075 'url' => 'http://example.com/images', 00076 'hashLevels' => 2, 00077 'transformVia404' => false, 00078 'backend' => 'local-backend' 00079 ); 00080 $tmpGlobals['wgForeignFileRepos'] = array(); 00081 $tmpGlobals['wgDefaultExternalStore'] = array(); 00082 $tmpGlobals['wgEnableParserCache'] = false; 00083 $tmpGlobals['wgCapitalLinks'] = true; 00084 $tmpGlobals['wgNoFollowLinks'] = true; 00085 $tmpGlobals['wgNoFollowDomainExceptions'] = array(); 00086 $tmpGlobals['wgExternalLinkTarget'] = false; 00087 $tmpGlobals['wgThumbnailScriptPath'] = false; 00088 $tmpGlobals['wgUseImageResize'] = true; 00089 $tmpGlobals['wgAllowExternalImages'] = true; 00090 $tmpGlobals['wgRawHtml'] = false; 00091 $tmpGlobals['wgUseTidy'] = false; 00092 $tmpGlobals['wgAlwaysUseTidy'] = false; 00093 $tmpGlobals['wgWellFormedXml'] = true; 00094 $tmpGlobals['wgAllowMicrodataAttributes'] = true; 00095 $tmpGlobals['wgExperimentalHtmlIds'] = false; 00096 $tmpGlobals['wgAdaptiveMessageCache'] = true; 00097 $tmpGlobals['wgUseDatabaseMessages'] = true; 00098 $tmpGlobals['wgLocaltimezone'] = 'UTC'; 00099 $tmpGlobals['wgDeferredUpdateList'] = array(); 00100 $tmpGlobals['wgGroupPermissions'] = array( 00101 '*' => array( 00102 'createaccount' => true, 00103 'read' => true, 00104 'edit' => true, 00105 'createpage' => true, 00106 'createtalk' => true, 00107 ) ); 00108 $tmpGlobals['wgNamespaceProtection'] = array( NS_MEDIAWIKI => 'editinterface' ); 00109 00110 $tmpGlobals['wgParser'] = new StubObject( 00111 'wgParser', $GLOBALS['wgParserConf']['class'], 00112 array( $GLOBALS['wgParserConf'] ) ); 00113 00114 $tmpGlobals['wgFileExtensions'][] = 'svg'; 00115 $tmpGlobals['wgSVGConverter'] = 'rsvg'; 00116 $tmpGlobals['wgSVGConverters']['rsvg'] = 00117 '$path/rsvg-convert -w $width -h $height $input -o $output'; 00118 00119 if ( $GLOBALS['wgStyleDirectory'] === false ) { 00120 $tmpGlobals['wgStyleDirectory'] = "$IP/skins"; 00121 } 00122 00123 # Replace all media handlers with a mock. We do not need to generate 00124 # actual thumbnails to do parser testing, we only care about receiving 00125 # a ThumbnailImage properly initialized. 00126 global $wgMediaHandlers; 00127 foreach ( $wgMediaHandlers as $type => $handler ) { 00128 $tmpGlobals['wgMediaHandlers'][$type] = 'MockBitmapHandler'; 00129 } 00130 // Vector images have to be handled slightly differently 00131 $tmpGlobals['wgMediaHandlers']['image/svg+xml'] = 'MockSvgHandler'; 00132 00133 $tmpHooks = $wgHooks; 00134 $tmpHooks['ParserTestParser'][] = 'ParserTestParserHook::setup'; 00135 $tmpHooks['ParserGetVariableValueTs'][] = 'ParserTest::getFakeTimestamp'; 00136 $tmpGlobals['wgHooks'] = $tmpHooks; 00137 # add a namespace shadowing a interwiki link, to test 00138 # proper precedence when resolving links. (bug 51680) 00139 $tmpGlobals['wgExtraNamespaces'] = array( 100 => 'MemoryAlpha' ); 00140 00141 $this->setMwGlobals( $tmpGlobals ); 00142 00143 $this->savedWeirdGlobals['image_alias'] = $wgNamespaceAliases['Image']; 00144 $this->savedWeirdGlobals['image_talk_alias'] = $wgNamespaceAliases['Image_talk']; 00145 00146 $wgNamespaceAliases['Image'] = NS_FILE; 00147 $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK; 00148 00149 MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache 00150 $wgContLang->resetNamespaces(); # reset namespace cache 00151 } 00152 00153 protected function tearDown() { 00154 global $wgNamespaceAliases, $wgContLang; 00155 00156 $wgNamespaceAliases['Image'] = $this->savedWeirdGlobals['image_alias']; 00157 $wgNamespaceAliases['Image_talk'] = $this->savedWeirdGlobals['image_talk_alias']; 00158 00159 // Restore backends 00160 RepoGroup::destroySingleton(); 00161 FileBackendGroup::destroySingleton(); 00162 00163 // Remove temporary pages from the link cache 00164 LinkCache::singleton()->clear(); 00165 00166 // Restore message cache (temporary pages and $wgUseDatabaseMessages) 00167 MessageCache::destroyInstance(); 00168 00169 parent::tearDown(); 00170 00171 MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache 00172 $wgContLang->resetNamespaces(); # reset namespace cache 00173 } 00174 00175 public static function tearDownAfterClass() { 00176 ParserTest::tearDownInterwikis(); 00177 parent::tearDownAfterClass(); 00178 } 00179 00180 function addDBData() { 00181 $this->tablesUsed[] = 'site_stats'; 00182 # disabled for performance 00183 #$this->tablesUsed[] = 'image'; 00184 00185 # Update certain things in site_stats 00186 $this->db->insert( 'site_stats', 00187 array( 'ss_row_id' => 1, 'ss_images' => 2, 'ss_good_articles' => 1 ), 00188 __METHOD__ 00189 ); 00190 00191 $user = User::newFromId( 0 ); 00192 LinkCache::singleton()->clear(); # Avoids the odd failure at creating the nullRevision 00193 00194 # Upload DB table entries for files. 00195 # We will upload the actual files later. Note that if anything causes LocalFile::load() 00196 # to be triggered before then, it will break via maybeUpgrade() setting the fileExists 00197 # member to false and storing it in cache. 00198 # note that the size/width/height/bits/etc of the file 00199 # are actually set by inspecting the file itself; the arguments 00200 # to recordUpload2 have no effect. That said, we try to make things 00201 # match up so it is less confusing to readers of the code & tests. 00202 $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.jpg' ) ); 00203 if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) { 00204 $image->recordUpload2( 00205 '', // archive name 00206 'Upload of some lame file', 00207 'Some lame file', 00208 array( 00209 'size' => 7881, 00210 'width' => 1941, 00211 'height' => 220, 00212 'bits' => 8, 00213 'media_type' => MEDIATYPE_BITMAP, 00214 'mime' => 'image/jpeg', 00215 'metadata' => serialize( array() ), 00216 'sha1' => wfBaseConvert( '1', 16, 36, 31 ), 00217 'fileExists' => true ), 00218 $this->db->timestamp( '20010115123500' ), $user 00219 ); 00220 } 00221 00222 $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Thumb.png' ) ); 00223 if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) { 00224 $image->recordUpload2( 00225 '', // archive name 00226 'Upload of some lame thumbnail', 00227 'Some lame thumbnail', 00228 array( 00229 'size' => 22589, 00230 'width' => 135, 00231 'height' => 135, 00232 'bits' => 8, 00233 'media_type' => MEDIATYPE_BITMAP, 00234 'mime' => 'image/png', 00235 'metadata' => serialize( array() ), 00236 'sha1' => wfBaseConvert( '2', 16, 36, 31 ), 00237 'fileExists' => true ), 00238 $this->db->timestamp( '20130225203040' ), $user 00239 ); 00240 } 00241 00242 # This image will be blacklisted in [[MediaWiki:Bad image list]] 00243 $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Bad.jpg' ) ); 00244 if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) { 00245 $image->recordUpload2( 00246 '', // archive name 00247 'zomgnotcensored', 00248 'Borderline image', 00249 array( 00250 'size' => 12345, 00251 'width' => 320, 00252 'height' => 240, 00253 'bits' => 24, 00254 'media_type' => MEDIATYPE_BITMAP, 00255 'mime' => 'image/jpeg', 00256 'metadata' => serialize( array() ), 00257 'sha1' => wfBaseConvert( '3', 16, 36, 31 ), 00258 'fileExists' => true ), 00259 $this->db->timestamp( '20010115123500' ), $user 00260 ); 00261 } 00262 $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.svg' ) ); 00263 if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) { 00264 $image->recordUpload2( '', 'Upload of some lame SVG', 'Some lame SVG', array( 00265 'size' => 12345, 00266 'width' => 200, 00267 'height' => 200, 00268 'bits' => 24, 00269 'media_type' => MEDIATYPE_DRAWING, 00270 'mime' => 'image/svg+xml', 00271 'metadata' => serialize( array() ), 00272 'sha1' => wfBaseConvert( '', 16, 36, 31 ), 00273 'fileExists' => true 00274 ), $this->db->timestamp( '20010115123500' ), $user ); 00275 } 00276 } 00277 00278 //ParserTest setup/teardown functions 00279 00284 protected function setupGlobals( $opts = array(), $config = '' ) { 00285 global $wgFileBackends; 00286 # Find out values for some special options. 00287 $lang = 00288 self::getOptionValue( 'language', $opts, 'en' ); 00289 $variant = 00290 self::getOptionValue( 'variant', $opts, false ); 00291 $maxtoclevel = 00292 self::getOptionValue( 'wgMaxTocLevel', $opts, 999 ); 00293 $linkHolderBatchSize = 00294 self::getOptionValue( 'wgLinkHolderBatchSize', $opts, 1000 ); 00295 00296 $uploadDir = $this->getUploadDir(); 00297 if ( $this->getCliArg( 'use-filebackend=' ) ) { 00298 if ( self::$backendToUse ) { 00299 $backend = self::$backendToUse; 00300 } else { 00301 $name = $this->getCliArg( 'use-filebackend=' ); 00302 $useConfig = array(); 00303 foreach ( $wgFileBackends as $conf ) { 00304 if ( $conf['name'] == $name ) { 00305 $useConfig = $conf; 00306 } 00307 } 00308 $useConfig['name'] = 'local-backend'; // swap name 00309 $class = $conf['class']; 00310 self::$backendToUse = new $class( $useConfig ); 00311 $backend = self::$backendToUse; 00312 } 00313 } else { 00314 # Replace with a mock. We do not care about generating real 00315 # files on the filesystem, just need to expose the file 00316 # informations. 00317 $backend = new MockFileBackend( array( 00318 'name' => 'local-backend', 00319 'lockManager' => 'nullLockManager', 00320 'containerPaths' => array( 00321 'local-public' => "$uploadDir", 00322 'local-thumb' => "$uploadDir/thumb", 00323 ) 00324 ) ); 00325 } 00326 00327 $settings = array( 00328 'wgLocalFileRepo' => array( 00329 'class' => 'LocalRepo', 00330 'name' => 'local', 00331 'url' => 'http://example.com/images', 00332 'hashLevels' => 2, 00333 'transformVia404' => false, 00334 'backend' => $backend 00335 ), 00336 'wgEnableUploads' => self::getOptionValue( 'wgEnableUploads', $opts, true ), 00337 'wgLanguageCode' => $lang, 00338 'wgDBprefix' => $this->db->getType() != 'oracle' ? 'unittest_' : 'ut_', 00339 'wgRawHtml' => self::getOptionValue( 'wgRawHtml', $opts, false ), 00340 'wgNamespacesWithSubpages' => array( NS_MAIN => isset( $opts['subpage'] ) ), 00341 'wgAllowExternalImages' => self::getOptionValue( 'wgAllowExternalImages', $opts, true ), 00342 'wgMaxTocLevel' => $maxtoclevel, 00343 'wgUseTeX' => isset( $opts['math'] ) || isset( $opts['texvc'] ), 00344 'wgMathDirectory' => $uploadDir . '/math', 00345 'wgDefaultLanguageVariant' => $variant, 00346 'wgLinkHolderBatchSize' => $linkHolderBatchSize, 00347 ); 00348 00349 if ( $config ) { 00350 $configLines = explode( "\n", $config ); 00351 00352 foreach ( $configLines as $line ) { 00353 list( $var, $value ) = explode( '=', $line, 2 ); 00354 00355 $settings[$var] = eval( "return $value;" ); //??? 00356 } 00357 } 00358 00359 $this->savedGlobals = array(); 00360 00362 wfRunHooks( 'ParserTestGlobals', array( &$settings ) ); 00363 00364 $langObj = Language::factory( $lang ); 00365 $settings['wgContLang'] = $langObj; 00366 $settings['wgLang'] = $langObj; 00367 00368 $context = new RequestContext(); 00369 $settings['wgOut'] = $context->getOutput(); 00370 $settings['wgUser'] = $context->getUser(); 00371 $settings['wgRequest'] = $context->getRequest(); 00372 00373 foreach ( $settings as $var => $val ) { 00374 if ( array_key_exists( $var, $GLOBALS ) ) { 00375 $this->savedGlobals[$var] = $GLOBALS[$var]; 00376 } 00377 00378 $GLOBALS[$var] = $val; 00379 } 00380 00381 MagicWord::clearCache(); 00382 00383 # The entries saved into RepoGroup cache with previous globals will be wrong. 00384 RepoGroup::destroySingleton(); 00385 FileBackendGroup::destroySingleton(); 00386 00387 # Create dummy files in storage 00388 $this->setupUploads(); 00389 00390 # Publish the articles after we have the final language set 00391 $this->publishTestArticles(); 00392 00393 MessageCache::destroyInstance(); 00394 00395 return $context; 00396 } 00397 00403 protected function getUploadDir() { 00404 if ( $this->keepUploads ) { 00405 $dir = wfTempDir() . '/mwParser-images'; 00406 00407 if ( is_dir( $dir ) ) { 00408 return $dir; 00409 } 00410 } else { 00411 $dir = wfTempDir() . "/mwParser-" . mt_rand() . "-images"; 00412 } 00413 00414 // wfDebug( "Creating upload directory $dir\n" ); 00415 if ( file_exists( $dir ) ) { 00416 wfDebug( "Already exists!\n" ); 00417 00418 return $dir; 00419 } 00420 00421 return $dir; 00422 } 00423 00430 protected function setupUploads() { 00431 global $IP; 00432 00433 $base = $this->getBaseDir(); 00434 $backend = RepoGroup::singleton()->getLocalRepo()->getBackend(); 00435 $backend->prepare( array( 'dir' => "$base/local-public/3/3a" ) ); 00436 $backend->store( array( 00437 'src' => "$IP/skins/monobook/headbg.jpg", 'dst' => "$base/local-public/3/3a/Foobar.jpg" 00438 ) ); 00439 $backend->prepare( array( 'dir' => "$base/local-public/e/ea" ) ); 00440 $backend->store( array( 00441 'src' => "$IP/skins/monobook/wiki.png", 'dst' => "$base/local-public/e/ea/Thumb.png" 00442 ) ); 00443 $backend->prepare( array( 'dir' => "$base/local-public/0/09" ) ); 00444 $backend->store( array( 00445 'src' => "$IP/skins/monobook/headbg.jpg", 'dst' => "$base/local-public/0/09/Bad.jpg" 00446 ) ); 00447 00448 // No helpful SVG file to copy, so make one ourselves 00449 $tmpDir = wfTempDir(); 00450 $tempFsFile = new TempFSFile( "$tmpDir/Foobar.svg" ); 00451 $tempFsFile->autocollect(); // destroy file when $tempFsFile leaves scope 00452 file_put_contents( "$tmpDir/Foobar.svg", 00453 '<?xml version="1.0" encoding="utf-8"?>' . 00454 '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><text>Foo</text></svg>' ); 00455 00456 $backend->prepare( array( 'dir' => "$base/local-public/f/ff" ) ); 00457 $backend->quickStore( array( 00458 'src' => "$tmpDir/Foobar.svg", 'dst' => "$base/local-public/f/ff/Foobar.svg" 00459 ) ); 00460 } 00461 00466 protected function teardownGlobals() { 00467 $this->teardownUploads(); 00468 00469 foreach ( $this->savedGlobals as $var => $val ) { 00470 $GLOBALS[$var] = $val; 00471 } 00472 } 00473 00477 private function teardownUploads() { 00478 if ( $this->keepUploads ) { 00479 return; 00480 } 00481 00482 $backend = RepoGroup::singleton()->getLocalRepo()->getBackend(); 00483 if ( $backend instanceof MockFileBackend ) { 00484 # In memory backend, so dont bother cleaning them up. 00485 return; 00486 } 00487 00488 $base = $this->getBaseDir(); 00489 // delete the files first, then the dirs. 00490 self::deleteFiles( 00491 array( 00492 "$base/local-public/3/3a/Foobar.jpg", 00493 "$base/local-thumb/3/3a/Foobar.jpg/180px-Foobar.jpg", 00494 "$base/local-thumb/3/3a/Foobar.jpg/200px-Foobar.jpg", 00495 "$base/local-thumb/3/3a/Foobar.jpg/640px-Foobar.jpg", 00496 "$base/local-thumb/3/3a/Foobar.jpg/120px-Foobar.jpg", 00497 "$base/local-thumb/3/3a/Foobar.jpg/1280px-Foobar.jpg", 00498 "$base/local-thumb/3/3a/Foobar.jpg/20px-Foobar.jpg", 00499 "$base/local-thumb/3/3a/Foobar.jpg/270px-Foobar.jpg", 00500 "$base/local-thumb/3/3a/Foobar.jpg/300px-Foobar.jpg", 00501 "$base/local-thumb/3/3a/Foobar.jpg/30px-Foobar.jpg", 00502 "$base/local-thumb/3/3a/Foobar.jpg/360px-Foobar.jpg", 00503 "$base/local-thumb/3/3a/Foobar.jpg/400px-Foobar.jpg", 00504 "$base/local-thumb/3/3a/Foobar.jpg/40px-Foobar.jpg", 00505 "$base/local-thumb/3/3a/Foobar.jpg/70px-Foobar.jpg", 00506 "$base/local-thumb/3/3a/Foobar.jpg/960px-Foobar.jpg", 00507 00508 "$base/local-public/e/ea/Thumb.png", 00509 00510 "$base/local-public/0/09/Bad.jpg", 00511 00512 "$base/local-public/f/ff/Foobar.svg", 00513 "$base/local-thumb/f/ff/Foobar.svg/180px-Foobar.svg.jpg", 00514 "$base/local-thumb/f/ff/Foobar.svg/270px-Foobar.svg.jpg", 00515 "$base/local-thumb/f/ff/Foobar.svg/360px-Foobar.svg.jpg", 00516 "$base/local-thumb/f/ff/Foobar.svg/langde-180px-Foobar.svg.jpg", 00517 "$base/local-thumb/f/ff/Foobar.svg/langde-270px-Foobar.svg.jpg", 00518 "$base/local-thumb/f/ff/Foobar.svg/langde-360px-Foobar.svg.jpg", 00519 00520 "$base/local-public/math/f/a/5/fa50b8b616463173474302ca3e63586b.png", 00521 ) 00522 ); 00523 } 00524 00529 private static function deleteFiles( $files ) { 00530 $backend = RepoGroup::singleton()->getLocalRepo()->getBackend(); 00531 foreach ( $files as $file ) { 00532 $backend->delete( array( 'src' => $file ), array( 'force' => 1 ) ); 00533 } 00534 foreach ( $files as $file ) { 00535 $tmp = $file; 00536 while ( $tmp = FileBackend::parentStoragePath( $tmp ) ) { 00537 if ( !$backend->clean( array( 'dir' => $tmp ) )->isOK() ) { 00538 break; 00539 } 00540 } 00541 } 00542 } 00543 00544 protected function getBaseDir() { 00545 return 'mwstore://local-backend'; 00546 } 00547 00548 public function parserTestProvider() { 00549 if ( $this->file === false ) { 00550 global $wgParserTestFiles; 00551 $this->file = $wgParserTestFiles[0]; 00552 } 00553 00554 return new TestFileIterator( $this->file, $this ); 00555 } 00556 00560 public function setParserTestFile( $filename ) { 00561 $this->file = $filename; 00562 } 00563 00568 public function testParserTest( $desc, $input, $result, $opts, $config ) { 00569 if ( $this->regex != '' && !preg_match( '/' . $this->regex . '/', $desc ) ) { 00570 $this->assertTrue( true ); // XXX: don't flood output with "test made no assertions" 00571 //$this->markTestSkipped( 'Filtered out by the user' ); 00572 return; 00573 } 00574 00575 if ( !$this->isWikitextNS( NS_MAIN ) ) { 00576 // parser tests frequently assume that the main namespace contains wikitext. 00577 // @todo When setting up pages, force the content model. Only skip if 00578 // $wgtContentModelUseDB is false. 00579 $this->markTestSkipped( "Main namespace does not support wikitext," 00580 . "skipping parser test: $desc" ); 00581 } 00582 00583 wfDebug( "Running parser test: $desc\n" ); 00584 00585 $opts = $this->parseOptions( $opts ); 00586 $context = $this->setupGlobals( $opts, $config ); 00587 00588 $user = $context->getUser(); 00589 $options = ParserOptions::newFromContext( $context ); 00590 00591 if ( isset( $opts['title'] ) ) { 00592 $titleText = $opts['title']; 00593 } else { 00594 $titleText = 'Parser test'; 00595 } 00596 00597 $local = isset( $opts['local'] ); 00598 $preprocessor = isset( $opts['preprocessor'] ) ? $opts['preprocessor'] : null; 00599 $parser = $this->getParser( $preprocessor ); 00600 00601 $title = Title::newFromText( $titleText ); 00602 00603 # Parser test requiring math. Make sure texvc is executable 00604 # or just skip such tests. 00605 if ( isset( $opts['math'] ) || isset( $opts['texvc'] ) ) { 00606 global $wgTexvc; 00607 00608 if ( !isset( $wgTexvc ) ) { 00609 $this->markTestSkipped( "SKIPPED: \$wgTexvc is not set" ); 00610 } elseif ( !is_executable( $wgTexvc ) ) { 00611 $this->markTestSkipped( "SKIPPED: texvc binary does not exist" 00612 . " or is not executable.\n" 00613 . "Current configuration is:\n\$wgTexvc = '$wgTexvc'" ); 00614 } 00615 } 00616 00617 if ( isset( $opts['pst'] ) ) { 00618 $out = $parser->preSaveTransform( $input, $title, $user, $options ); 00619 } elseif ( isset( $opts['msg'] ) ) { 00620 $out = $parser->transformMsg( $input, $options, $title ); 00621 } elseif ( isset( $opts['section'] ) ) { 00622 $section = $opts['section']; 00623 $out = $parser->getSection( $input, $section ); 00624 } elseif ( isset( $opts['replace'] ) ) { 00625 $section = $opts['replace'][0]; 00626 $replace = $opts['replace'][1]; 00627 $out = $parser->replaceSection( $input, $section, $replace ); 00628 } elseif ( isset( $opts['comment'] ) ) { 00629 $out = Linker::formatComment( $input, $title, $local ); 00630 } elseif ( isset( $opts['preload'] ) ) { 00631 $out = $parser->getPreloadText( $input, $title, $options ); 00632 } else { 00633 $output = $parser->parse( $input, $title, $options, true, true, 1337 ); 00634 $output->setTOCEnabled( !isset( $opts['notoc'] ) ); 00635 $out = $output->getText(); 00636 00637 if ( isset( $opts['showtitle'] ) ) { 00638 if ( $output->getTitleText() ) { 00639 $title = $output->getTitleText(); 00640 } 00641 00642 $out = "$title\n$out"; 00643 } 00644 00645 if ( isset( $opts['ill'] ) ) { 00646 $out = $this->tidy( implode( ' ', $output->getLanguageLinks() ) ); 00647 } elseif ( isset( $opts['cat'] ) ) { 00648 $outputPage = $context->getOutput(); 00649 $outputPage->addCategoryLinks( $output->getCategories() ); 00650 $cats = $outputPage->getCategoryLinks(); 00651 00652 if ( isset( $cats['normal'] ) ) { 00653 $out = $this->tidy( implode( ' ', $cats['normal'] ) ); 00654 } else { 00655 $out = ''; 00656 } 00657 } 00658 $parser->mPreprocessor = null; 00659 00660 $result = $this->tidy( $result ); 00661 } 00662 00663 $this->teardownGlobals(); 00664 00665 $this->assertEquals( $result, $out, $desc ); 00666 } 00667 00676 public function testFuzzTests() { 00677 global $wgParserTestFiles; 00678 00679 $files = $wgParserTestFiles; 00680 00681 if ( $this->getCliArg( 'file=' ) ) { 00682 $files = array( $this->getCliArg( 'file=' ) ); 00683 } 00684 00685 $dict = $this->getFuzzInput( $files ); 00686 $dictSize = strlen( $dict ); 00687 $logMaxLength = log( $this->maxFuzzTestLength ); 00688 00689 ini_set( 'memory_limit', $this->memoryLimit * 1048576 ); 00690 00691 $user = new User; 00692 $opts = ParserOptions::newFromUser( $user ); 00693 $title = Title::makeTitle( NS_MAIN, 'Parser_test' ); 00694 00695 $id = 1; 00696 00697 while ( true ) { 00698 00699 // Generate test input 00700 mt_srand( ++$this->fuzzSeed ); 00701 $totalLength = mt_rand( 1, $this->maxFuzzTestLength ); 00702 $input = ''; 00703 00704 while ( strlen( $input ) < $totalLength ) { 00705 $logHairLength = mt_rand( 0, 1000000 ) / 1000000 * $logMaxLength; 00706 $hairLength = min( intval( exp( $logHairLength ) ), $dictSize ); 00707 $offset = mt_rand( 0, $dictSize - $hairLength ); 00708 $input .= substr( $dict, $offset, $hairLength ); 00709 } 00710 00711 $this->setupGlobals(); 00712 $parser = $this->getParser(); 00713 00714 // Run the test 00715 try { 00716 $parser->parse( $input, $title, $opts ); 00717 $this->assertTrue( true, "Test $id, fuzz seed {$this->fuzzSeed}" ); 00718 } catch ( Exception $exception ) { 00719 $input_dump = sprintf( "string(%d) \"%s\"\n", strlen( $input ), $input ); 00720 00721 $this->assertTrue( false, "Test $id, fuzz seed {$this->fuzzSeed}. \n\n" . 00722 "Input: $input_dump\n\nError: {$exception->getMessage()}\n\n" . 00723 "Backtrace: {$exception->getTraceAsString()}" ); 00724 } 00725 00726 $this->teardownGlobals(); 00727 $parser->__destruct(); 00728 00729 if ( $id % 100 == 0 ) { 00730 $usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 ); 00731 //echo "{$this->fuzzSeed}: $numSuccess/$numTotal (mem: $usage%)\n"; 00732 if ( $usage > 90 ) { 00733 $ret = "Out of memory:\n"; 00734 $memStats = $this->getMemoryBreakdown(); 00735 00736 foreach ( $memStats as $name => $usage ) { 00737 $ret .= "$name: $usage\n"; 00738 } 00739 00740 throw new MWException( $ret ); 00741 } 00742 } 00743 00744 $id++; 00745 } 00746 } 00747 00748 //Various getter functions 00749 00753 function getFuzzInput( $filenames ) { 00754 $dict = ''; 00755 00756 foreach ( $filenames as $filename ) { 00757 $contents = file_get_contents( $filename ); 00758 preg_match_all( '/!!\s*input\n(.*?)\n!!\s*result/s', $contents, $matches ); 00759 00760 foreach ( $matches[1] as $match ) { 00761 $dict .= $match . "\n"; 00762 } 00763 } 00764 00765 return $dict; 00766 } 00767 00771 function getMemoryBreakdown() { 00772 $memStats = array(); 00773 00774 foreach ( $GLOBALS as $name => $value ) { 00775 $memStats['$' . $name] = strlen( serialize( $value ) ); 00776 } 00777 00778 $classes = get_declared_classes(); 00779 00780 foreach ( $classes as $class ) { 00781 $rc = new ReflectionClass( $class ); 00782 $props = $rc->getStaticProperties(); 00783 $memStats[$class] = strlen( serialize( $props ) ); 00784 $methods = $rc->getMethods(); 00785 00786 foreach ( $methods as $method ) { 00787 $memStats[$class] += strlen( serialize( $method->getStaticVariables() ) ); 00788 } 00789 } 00790 00791 $functions = get_defined_functions(); 00792 00793 foreach ( $functions['user'] as $function ) { 00794 $rf = new ReflectionFunction( $function ); 00795 $memStats["$function()"] = strlen( serialize( $rf->getStaticVariables() ) ); 00796 } 00797 00798 asort( $memStats ); 00799 00800 return $memStats; 00801 } 00802 00806 function getParser( $preprocessor = null ) { 00807 global $wgParserConf; 00808 00809 $class = $wgParserConf['class']; 00810 $parser = new $class( array( 'preprocessorClass' => $preprocessor ) + $wgParserConf ); 00811 00812 wfRunHooks( 'ParserTestParser', array( &$parser ) ); 00813 00814 return $parser; 00815 } 00816 00817 //Various action functions 00818 00819 public function addArticle( $name, $text, $line ) { 00820 self::$articles[$name] = array( $text, $line ); 00821 } 00822 00823 public function publishTestArticles() { 00824 if ( empty( self::$articles ) ) { 00825 return; 00826 } 00827 00828 foreach ( self::$articles as $name => $info ) { 00829 list( $text, $line ) = $info; 00830 ParserTest::addArticle( $name, $text, $line, 'ignoreduplicate' ); 00831 } 00832 } 00833 00842 public function requireHook( $name ) { 00843 global $wgParser; 00844 $wgParser->firstCallInit(); // make sure hooks are loaded. 00845 return isset( $wgParser->mTagHooks[$name] ); 00846 } 00847 00848 public function requireFunctionHook( $name ) { 00849 global $wgParser; 00850 $wgParser->firstCallInit(); // make sure hooks are loaded. 00851 return isset( $wgParser->mFunctionHooks[$name] ); 00852 } 00853 00854 //Various "cleanup" functions 00855 00863 protected function tidy( $text ) { 00864 global $wgUseTidy; 00865 00866 if ( $wgUseTidy ) { 00867 $text = MWTidy::tidy( $text ); 00868 } 00869 00870 return $text; 00871 } 00872 00876 public function removeEndingNewline( $s ) { 00877 if ( substr( $s, -1 ) === "\n" ) { 00878 return substr( $s, 0, -1 ); 00879 } else { 00880 return $s; 00881 } 00882 } 00883 00884 //Test options parser functions 00885 00886 protected function parseOptions( $instring ) { 00887 $opts = array(); 00888 // foo 00889 // foo=bar 00890 // foo="bar baz" 00891 // foo=[[bar baz]] 00892 // foo=bar,"baz quux" 00893 $regex = '/\b 00894 ([\w-]+) # Key 00895 \b 00896 (?:\s* 00897 = # First sub-value 00898 \s* 00899 ( 00900 " 00901 [^"]* # Quoted val 00902 " 00903 | 00904 \[\[ 00905 [^]]* # Link target 00906 \]\] 00907 | 00908 [\w-]+ # Plain word 00909 ) 00910 (?:\s* 00911 , # Sub-vals 1..N 00912 \s* 00913 ( 00914 "[^"]*" # Quoted val 00915 | 00916 \[\[[^]]*\]\] # Link target 00917 | 00918 [\w-]+ # Plain word 00919 ) 00920 )* 00921 )? 00922 /x'; 00923 00924 if ( preg_match_all( $regex, $instring, $matches, PREG_SET_ORDER ) ) { 00925 foreach ( $matches as $bits ) { 00926 array_shift( $bits ); 00927 $key = strtolower( array_shift( $bits ) ); 00928 if ( count( $bits ) == 0 ) { 00929 $opts[$key] = true; 00930 } elseif ( count( $bits ) == 1 ) { 00931 $opts[$key] = $this->cleanupOption( array_shift( $bits ) ); 00932 } else { 00933 // Array! 00934 $opts[$key] = array_map( array( $this, 'cleanupOption' ), $bits ); 00935 } 00936 } 00937 } 00938 00939 return $opts; 00940 } 00941 00942 protected function cleanupOption( $opt ) { 00943 if ( substr( $opt, 0, 1 ) == '"' ) { 00944 return substr( $opt, 1, -1 ); 00945 } 00946 00947 if ( substr( $opt, 0, 2 ) == '[[' ) { 00948 return substr( $opt, 2, -2 ); 00949 } 00950 00951 return $opt; 00952 } 00953 00960 protected static function getOptionValue( $key, $opts, $default ) { 00961 $key = strtolower( $key ); 00962 00963 if ( isset( $opts[$key] ) ) { 00964 return $opts[$key]; 00965 } else { 00966 return $default; 00967 } 00968 } 00969 }