MediaWiki  REL1_24
NewParserTest.php
Go to the documentation of this file.
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     public $transparentHooks = array();
00029 
00030     //Fuzz test
00031     public $maxFuzzTestLength = 300;
00032     public $fuzzSeed = 0;
00033     public $memoryLimit = 50;
00034 
00038     private $djVuSupport;
00042     private $tidySupport;
00043 
00044     protected $file = false;
00045 
00046     public static function setUpBeforeClass() {
00047         // Inject ParserTest well-known interwikis
00048         ParserTest::setupInterwikis();
00049     }
00050 
00051     protected function setUp() {
00052         global $wgNamespaceAliases, $wgContLang;
00053         global $wgHooks, $IP;
00054 
00055         parent::setUp();
00056 
00057         //Setup CLI arguments
00058         if ( $this->getCliArg( 'regex' ) ) {
00059             $this->regex = $this->getCliArg( 'regex' );
00060         } else {
00061             # Matches anything
00062             $this->regex = '';
00063         }
00064 
00065         $this->keepUploads = $this->getCliArg( 'keep-uploads' );
00066 
00067         $tmpGlobals = array();
00068 
00069         $tmpGlobals['wgLanguageCode'] = 'en';
00070         $tmpGlobals['wgContLang'] = Language::factory( 'en' );
00071         $tmpGlobals['wgSitename'] = 'MediaWiki';
00072         $tmpGlobals['wgServer'] = 'http://example.org';
00073         $tmpGlobals['wgServerName'] = 'example.org';
00074         $tmpGlobals['wgScript'] = '/index.php';
00075         $tmpGlobals['wgScriptPath'] = '/';
00076         $tmpGlobals['wgArticlePath'] = '/wiki/$1';
00077         $tmpGlobals['wgActionPaths'] = array();
00078         $tmpGlobals['wgVariantArticlePath'] = false;
00079         $tmpGlobals['wgExtensionAssetsPath'] = '/extensions';
00080         $tmpGlobals['wgStylePath'] = '/skins';
00081         $tmpGlobals['wgEnableUploads'] = true;
00082         $tmpGlobals['wgUploadNavigationUrl'] = false;
00083         $tmpGlobals['wgThumbnailScriptPath'] = false;
00084         $tmpGlobals['wgLocalFileRepo'] = array(
00085             'class' => 'LocalRepo',
00086             'name' => 'local',
00087             'url' => 'http://example.com/images',
00088             'hashLevels' => 2,
00089             'transformVia404' => false,
00090             'backend' => 'local-backend'
00091         );
00092         $tmpGlobals['wgForeignFileRepos'] = array();
00093         $tmpGlobals['wgDefaultExternalStore'] = array();
00094         $tmpGlobals['wgEnableParserCache'] = false;
00095         $tmpGlobals['wgCapitalLinks'] = true;
00096         $tmpGlobals['wgNoFollowLinks'] = true;
00097         $tmpGlobals['wgNoFollowDomainExceptions'] = array();
00098         $tmpGlobals['wgExternalLinkTarget'] = false;
00099         $tmpGlobals['wgThumbnailScriptPath'] = false;
00100         $tmpGlobals['wgUseImageResize'] = true;
00101         $tmpGlobals['wgAllowExternalImages'] = true;
00102         $tmpGlobals['wgRawHtml'] = false;
00103         $tmpGlobals['wgWellFormedXml'] = true;
00104         $tmpGlobals['wgAllowMicrodataAttributes'] = true;
00105         $tmpGlobals['wgExperimentalHtmlIds'] = false;
00106         $tmpGlobals['wgAdaptiveMessageCache'] = true;
00107         $tmpGlobals['wgUseDatabaseMessages'] = true;
00108         $tmpGlobals['wgLocaltimezone'] = 'UTC';
00109         $tmpGlobals['wgDeferredUpdateList'] = array();
00110         $tmpGlobals['wgGroupPermissions'] = array(
00111             '*' => array(
00112                 'createaccount' => true,
00113                 'read' => true,
00114                 'edit' => true,
00115                 'createpage' => true,
00116                 'createtalk' => true,
00117         ) );
00118         $tmpGlobals['wgNamespaceProtection'] = array( NS_MEDIAWIKI => 'editinterface' );
00119 
00120         $tmpGlobals['wgParser'] = new StubObject(
00121             'wgParser', $GLOBALS['wgParserConf']['class'],
00122             array( $GLOBALS['wgParserConf'] ) );
00123 
00124         $tmpGlobals['wgFileExtensions'][] = 'svg';
00125         $tmpGlobals['wgSVGConverter'] = 'rsvg';
00126         $tmpGlobals['wgSVGConverters']['rsvg'] =
00127             '$path/rsvg-convert -w $width -h $height $input -o $output';
00128 
00129         if ( $GLOBALS['wgStyleDirectory'] === false ) {
00130             $tmpGlobals['wgStyleDirectory'] = "$IP/skins";
00131         }
00132 
00133         # Replace all media handlers with a mock. We do not need to generate
00134         # actual thumbnails to do parser testing, we only care about receiving
00135         # a ThumbnailImage properly initialized.
00136         global $wgMediaHandlers;
00137         foreach ( $wgMediaHandlers as $type => $handler ) {
00138             $tmpGlobals['wgMediaHandlers'][$type] = 'MockBitmapHandler';
00139         }
00140         // Vector images have to be handled slightly differently
00141         $tmpGlobals['wgMediaHandlers']['image/svg+xml'] = 'MockSvgHandler';
00142 
00143         // DjVu images have to be handled slightly differently
00144         $tmpGlobals['wgMediaHandlers']['image/vnd.djvu'] = 'MockDjVuHandler';
00145 
00146         $tmpHooks = $wgHooks;
00147         $tmpHooks['ParserTestParser'][] = 'ParserTestParserHook::setup';
00148         $tmpHooks['ParserGetVariableValueTs'][] = 'ParserTest::getFakeTimestamp';
00149         $tmpGlobals['wgHooks'] = $tmpHooks;
00150         # add a namespace shadowing a interwiki link, to test
00151         # proper precedence when resolving links. (bug 51680)
00152         $tmpGlobals['wgExtraNamespaces'] = array( 100 => 'MemoryAlpha' );
00153 
00154         $tmpGlobals['wgLocalInterwikis'] = array( 'local', 'mi' );
00155         # "extra language links"
00156         # see https://gerrit.wikimedia.org/r/111390
00157         $tmpGlobals['wgExtraInterlanguageLinkPrefixes'] = array( 'mul' );
00158 
00159         // DjVu support
00160         $this->djVuSupport = new DjVuSupport();
00161         // Tidy support
00162         $this->tidySupport = new TidySupport();
00163         // We always set 'wgUseTidy' to false when parsing, but certain
00164         // test-running modes still use tidy if available, so ensure
00165         // that the tidy-related options are all set to their defaults.
00166         $tmpGlobals['wgUseTidy'] = false;
00167         $tmpGlobals['wgAlwaysUseTidy'] = false;
00168         $tmpGlobals['wgDebugTidy'] = false;
00169         $tmpGlobals['wgTidyConf'] = $IP . '/includes/tidy.conf';
00170         $tmpGlobals['wgTidyOpts'] = '';
00171         $tmpGlobals['wgTidyInternal'] = $this->tidySupport->isInternal();
00172 
00173         $this->setMwGlobals( $tmpGlobals );
00174 
00175         $this->savedWeirdGlobals['image_alias'] = $wgNamespaceAliases['Image'];
00176         $this->savedWeirdGlobals['image_talk_alias'] = $wgNamespaceAliases['Image_talk'];
00177 
00178         $wgNamespaceAliases['Image'] = NS_FILE;
00179         $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK;
00180 
00181         MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
00182         $wgContLang->resetNamespaces(); # reset namespace cache
00183     }
00184 
00185     protected function tearDown() {
00186         global $wgNamespaceAliases, $wgContLang;
00187 
00188         $wgNamespaceAliases['Image'] = $this->savedWeirdGlobals['image_alias'];
00189         $wgNamespaceAliases['Image_talk'] = $this->savedWeirdGlobals['image_talk_alias'];
00190 
00191         // Restore backends
00192         RepoGroup::destroySingleton();
00193         FileBackendGroup::destroySingleton();
00194 
00195         // Remove temporary pages from the link cache
00196         LinkCache::singleton()->clear();
00197 
00198         // Restore message cache (temporary pages and $wgUseDatabaseMessages)
00199         MessageCache::destroyInstance();
00200 
00201         parent::tearDown();
00202 
00203         MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
00204         $wgContLang->resetNamespaces(); # reset namespace cache
00205     }
00206 
00207     public static function tearDownAfterClass() {
00208         ParserTest::tearDownInterwikis();
00209         parent::tearDownAfterClass();
00210     }
00211 
00212     function addDBData() {
00213         $this->tablesUsed[] = 'site_stats';
00214         # disabled for performance
00215         #$this->tablesUsed[] = 'image';
00216 
00217         # Update certain things in site_stats
00218         $this->db->insert( 'site_stats',
00219             array( 'ss_row_id' => 1, 'ss_images' => 2, 'ss_good_articles' => 1 ),
00220             __METHOD__
00221         );
00222 
00223         $user = User::newFromId( 0 );
00224         LinkCache::singleton()->clear(); # Avoids the odd failure at creating the nullRevision
00225 
00226         # Upload DB table entries for files.
00227         # We will upload the actual files later. Note that if anything causes LocalFile::load()
00228         # to be triggered before then, it will break via maybeUpgrade() setting the fileExists
00229         # member to false and storing it in cache.
00230         # note that the size/width/height/bits/etc of the file
00231         # are actually set by inspecting the file itself; the arguments
00232         # to recordUpload2 have no effect.  That said, we try to make things
00233         # match up so it is less confusing to readers of the code & tests.
00234         $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.jpg' ) );
00235         if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) {
00236             $image->recordUpload2(
00237                 '', // archive name
00238                 'Upload of some lame file',
00239                 'Some lame file',
00240                 array(
00241                     'size' => 7881,
00242                     'width' => 1941,
00243                     'height' => 220,
00244                     'bits' => 8,
00245                     'media_type' => MEDIATYPE_BITMAP,
00246                     'mime' => 'image/jpeg',
00247                     'metadata' => serialize( array() ),
00248                     'sha1' => wfBaseConvert( '1', 16, 36, 31 ),
00249                     'fileExists' => true ),
00250                 $this->db->timestamp( '20010115123500' ), $user
00251             );
00252         }
00253 
00254         $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Thumb.png' ) );
00255         if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) {
00256             $image->recordUpload2(
00257                 '', // archive name
00258                 'Upload of some lame thumbnail',
00259                 'Some lame thumbnail',
00260                 array(
00261                     'size' => 22589,
00262                     'width' => 135,
00263                     'height' => 135,
00264                     'bits' => 8,
00265                     'media_type' => MEDIATYPE_BITMAP,
00266                     'mime' => 'image/png',
00267                     'metadata' => serialize( array() ),
00268                     'sha1' => wfBaseConvert( '2', 16, 36, 31 ),
00269                     'fileExists' => true ),
00270                 $this->db->timestamp( '20130225203040' ), $user
00271             );
00272         }
00273 
00274         # This image will be blacklisted in [[MediaWiki:Bad image list]]
00275         $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Bad.jpg' ) );
00276         if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) {
00277             $image->recordUpload2(
00278                 '', // archive name
00279                 'zomgnotcensored',
00280                 'Borderline image',
00281                 array(
00282                     'size' => 12345,
00283                     'width' => 320,
00284                     'height' => 240,
00285                     'bits' => 24,
00286                     'media_type' => MEDIATYPE_BITMAP,
00287                     'mime' => 'image/jpeg',
00288                     'metadata' => serialize( array() ),
00289                     'sha1' => wfBaseConvert( '3', 16, 36, 31 ),
00290                     'fileExists' => true ),
00291                 $this->db->timestamp( '20010115123500' ), $user
00292             );
00293         }
00294         $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.svg' ) );
00295         if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) {
00296             $image->recordUpload2( '', 'Upload of some lame SVG', 'Some lame SVG', array(
00297                     'size'        => 12345,
00298                     'width'       => 240,
00299                     'height'      => 180,
00300                     'bits'        => 0,
00301                     'media_type'  => MEDIATYPE_DRAWING,
00302                     'mime'        => 'image/svg+xml',
00303                     'metadata'    => serialize( array() ),
00304                     'sha1'        => wfBaseConvert( '', 16, 36, 31 ),
00305                     'fileExists'  => true
00306             ), $this->db->timestamp( '20010115123500' ), $user );
00307         }
00308 
00309         # A DjVu file
00310         $image = wfLocalFile( Title::makeTitle( NS_FILE, 'LoremIpsum.djvu' ) );
00311         if ( !$this->db->selectField( 'image', '1', array( 'img_name' => $image->getName() ) ) ) {
00312             $image->recordUpload2( '', 'Upload a DjVu', 'A DjVu', array(
00313                 'size' => 3249,
00314                 'width' => 2480,
00315                 'height' => 3508,
00316                 'bits' => 0,
00317                 'media_type' => MEDIATYPE_BITMAP,
00318                 'mime' => 'image/vnd.djvu',
00319                 'metadata' => '<?xml version="1.0" ?>
00320 <!DOCTYPE DjVuXML PUBLIC "-//W3C//DTD DjVuXML 1.1//EN" "pubtext/DjVuXML-s.dtd">
00321 <DjVuXML>
00322 <HEAD></HEAD>
00323 <BODY><OBJECT height="3508" width="2480">
00324 <PARAM name="DPI" value="300" />
00325 <PARAM name="GAMMA" value="2.2" />
00326 </OBJECT>
00327 <OBJECT height="3508" width="2480">
00328 <PARAM name="DPI" value="300" />
00329 <PARAM name="GAMMA" value="2.2" />
00330 </OBJECT>
00331 <OBJECT height="3508" width="2480">
00332 <PARAM name="DPI" value="300" />
00333 <PARAM name="GAMMA" value="2.2" />
00334 </OBJECT>
00335 <OBJECT height="3508" width="2480">
00336 <PARAM name="DPI" value="300" />
00337 <PARAM name="GAMMA" value="2.2" />
00338 </OBJECT>
00339 <OBJECT height="3508" width="2480">
00340 <PARAM name="DPI" value="300" />
00341 <PARAM name="GAMMA" value="2.2" />
00342 </OBJECT>
00343 </BODY>
00344 </DjVuXML>',
00345                 'sha1' => wfBaseConvert( '', 16, 36, 31 ),
00346                 'fileExists' => true
00347             ), $this->db->timestamp( '20140115123600' ), $user );
00348         }
00349     }
00350 
00351     //ParserTest setup/teardown functions
00352 
00360     protected function setupGlobals( $opts = array(), $config = '' ) {
00361         global $wgFileBackends;
00362         # Find out values for some special options.
00363         $lang =
00364             self::getOptionValue( 'language', $opts, 'en' );
00365         $variant =
00366             self::getOptionValue( 'variant', $opts, false );
00367         $maxtoclevel =
00368             self::getOptionValue( 'wgMaxTocLevel', $opts, 999 );
00369         $linkHolderBatchSize =
00370             self::getOptionValue( 'wgLinkHolderBatchSize', $opts, 1000 );
00371 
00372         $uploadDir = $this->getUploadDir();
00373         if ( $this->getCliArg( 'use-filebackend' ) ) {
00374             if ( self::$backendToUse ) {
00375                 $backend = self::$backendToUse;
00376             } else {
00377                 $name = $this->getCliArg( 'use-filebackend' );
00378                 $useConfig = array();
00379                 foreach ( $wgFileBackends as $conf ) {
00380                     if ( $conf['name'] == $name ) {
00381                         $useConfig = $conf;
00382                     }
00383                 }
00384                 $useConfig['name'] = 'local-backend'; // swap name
00385                 unset( $useConfig['lockManager'] );
00386                 unset( $useConfig['fileJournal'] );
00387                 $class = $useConfig['class'];
00388                 self::$backendToUse = new $class( $useConfig );
00389                 $backend = self::$backendToUse;
00390             }
00391         } else {
00392             # Replace with a mock. We do not care about generating real
00393             # files on the filesystem, just need to expose the file
00394             # informations.
00395             $backend = new MockFileBackend( array(
00396                 'name' => 'local-backend',
00397                 'wikiId' => wfWikiId()
00398             ) );
00399         }
00400 
00401         $settings = array(
00402             'wgLocalFileRepo' => array(
00403                 'class' => 'LocalRepo',
00404                 'name' => 'local',
00405                 'url' => 'http://example.com/images',
00406                 'hashLevels' => 2,
00407                 'transformVia404' => false,
00408                 'backend' => $backend
00409             ),
00410             'wgEnableUploads' => self::getOptionValue( 'wgEnableUploads', $opts, true ),
00411             'wgLanguageCode' => $lang,
00412             'wgDBprefix' => $this->db->getType() != 'oracle' ? 'unittest_' : 'ut_',
00413             'wgRawHtml' => self::getOptionValue( 'wgRawHtml', $opts, false ),
00414             'wgNamespacesWithSubpages' => array( NS_MAIN => isset( $opts['subpage'] ) ),
00415             'wgAllowExternalImages' => self::getOptionValue( 'wgAllowExternalImages', $opts, true ),
00416             'wgThumbLimits' => array( self::getOptionValue( 'thumbsize', $opts, 180 ) ),
00417             'wgMaxTocLevel' => $maxtoclevel,
00418             'wgUseTeX' => isset( $opts['math'] ) || isset( $opts['texvc'] ),
00419             'wgMathDirectory' => $uploadDir . '/math',
00420             'wgDefaultLanguageVariant' => $variant,
00421             'wgLinkHolderBatchSize' => $linkHolderBatchSize,
00422         );
00423 
00424         if ( $config ) {
00425             $configLines = explode( "\n", $config );
00426 
00427             foreach ( $configLines as $line ) {
00428                 list( $var, $value ) = explode( '=', $line, 2 );
00429 
00430                 $settings[$var] = eval( "return $value;" ); //???
00431             }
00432         }
00433 
00434         $this->savedGlobals = array();
00435 
00437         wfRunHooks( 'ParserTestGlobals', array( &$settings ) );
00438 
00439         $langObj = Language::factory( $lang );
00440         $settings['wgContLang'] = $langObj;
00441         $settings['wgLang'] = $langObj;
00442 
00443         $context = new RequestContext();
00444         $settings['wgOut'] = $context->getOutput();
00445         $settings['wgUser'] = $context->getUser();
00446         $settings['wgRequest'] = $context->getRequest();
00447 
00448         // We (re)set $wgThumbLimits to a single-element array above.
00449         $context->getUser()->setOption( 'thumbsize', 0 );
00450 
00451         foreach ( $settings as $var => $val ) {
00452             if ( array_key_exists( $var, $GLOBALS ) ) {
00453                 $this->savedGlobals[$var] = $GLOBALS[$var];
00454             }
00455 
00456             $GLOBALS[$var] = $val;
00457         }
00458 
00459         MagicWord::clearCache();
00460 
00461         # The entries saved into RepoGroup cache with previous globals will be wrong.
00462         RepoGroup::destroySingleton();
00463         FileBackendGroup::destroySingleton();
00464 
00465         # Create dummy files in storage
00466         $this->setupUploads();
00467 
00468         # Publish the articles after we have the final language set
00469         $this->publishTestArticles();
00470 
00471         MessageCache::destroyInstance();
00472 
00473         return $context;
00474     }
00475 
00481     protected function getUploadDir() {
00482         if ( $this->keepUploads ) {
00483             $dir = wfTempDir() . '/mwParser-images';
00484 
00485             if ( is_dir( $dir ) ) {
00486                 return $dir;
00487             }
00488         } else {
00489             $dir = wfTempDir() . "/mwParser-" . mt_rand() . "-images";
00490         }
00491 
00492         // wfDebug( "Creating upload directory $dir\n" );
00493         if ( file_exists( $dir ) ) {
00494             wfDebug( "Already exists!\n" );
00495 
00496             return $dir;
00497         }
00498 
00499         return $dir;
00500     }
00501 
00508     protected function setupUploads() {
00509         global $IP;
00510 
00511         $base = $this->getBaseDir();
00512         $backend = RepoGroup::singleton()->getLocalRepo()->getBackend();
00513         $backend->prepare( array( 'dir' => "$base/local-public/3/3a" ) );
00514         $backend->store( array(
00515             'src' => "$IP/tests/phpunit/data/parser/headbg.jpg",
00516             'dst' => "$base/local-public/3/3a/Foobar.jpg"
00517         ) );
00518         $backend->prepare( array( 'dir' => "$base/local-public/e/ea" ) );
00519         $backend->store( array(
00520             'src' => "$IP/tests/phpunit/data/parser/wiki.png",
00521             'dst' => "$base/local-public/e/ea/Thumb.png"
00522         ) );
00523         $backend->prepare( array( 'dir' => "$base/local-public/0/09" ) );
00524         $backend->store( array(
00525             'src' => "$IP/tests/phpunit/data/parser/headbg.jpg",
00526             'dst' => "$base/local-public/0/09/Bad.jpg"
00527         ) );
00528         $backend->prepare( array( 'dir' => "$base/local-public/5/5f" ) );
00529         $backend->store( array(
00530             'src' => "$IP/tests/phpunit/data/parser/LoremIpsum.djvu",
00531             'dst' => "$base/local-public/5/5f/LoremIpsum.djvu"
00532         ) );
00533 
00534         // No helpful SVG file to copy, so make one ourselves
00535         $data = '<?xml version="1.0" encoding="utf-8"?>' .
00536             '<svg xmlns="http://www.w3.org/2000/svg"' .
00537             ' version="1.1" width="240" height="180"/>';
00538 
00539         $backend->prepare( array( 'dir' => "$base/local-public/f/ff" ) );
00540         $backend->quickCreate( array(
00541             'content' => $data, 'dst' => "$base/local-public/f/ff/Foobar.svg"
00542         ) );
00543     }
00544 
00549     protected function teardownGlobals() {
00550         $this->teardownUploads();
00551 
00552         foreach ( $this->savedGlobals as $var => $val ) {
00553             $GLOBALS[$var] = $val;
00554         }
00555     }
00556 
00560     private function teardownUploads() {
00561         if ( $this->keepUploads ) {
00562             return;
00563         }
00564 
00565         $backend = RepoGroup::singleton()->getLocalRepo()->getBackend();
00566         if ( $backend instanceof MockFileBackend ) {
00567             # In memory backend, so dont bother cleaning them up.
00568             return;
00569         }
00570 
00571         $base = $this->getBaseDir();
00572         // delete the files first, then the dirs.
00573         self::deleteFiles(
00574             array(
00575                 "$base/local-public/3/3a/Foobar.jpg",
00576                 "$base/local-thumb/3/3a/Foobar.jpg/1000px-Foobar.jpg",
00577                 "$base/local-thumb/3/3a/Foobar.jpg/100px-Foobar.jpg",
00578                 "$base/local-thumb/3/3a/Foobar.jpg/120px-Foobar.jpg",
00579                 "$base/local-thumb/3/3a/Foobar.jpg/1280px-Foobar.jpg",
00580                 "$base/local-thumb/3/3a/Foobar.jpg/137px-Foobar.jpg",
00581                 "$base/local-thumb/3/3a/Foobar.jpg/1500px-Foobar.jpg",
00582                 "$base/local-thumb/3/3a/Foobar.jpg/177px-Foobar.jpg",
00583                 "$base/local-thumb/3/3a/Foobar.jpg/180px-Foobar.jpg",
00584                 "$base/local-thumb/3/3a/Foobar.jpg/200px-Foobar.jpg",
00585                 "$base/local-thumb/3/3a/Foobar.jpg/206px-Foobar.jpg",
00586                 "$base/local-thumb/3/3a/Foobar.jpg/20px-Foobar.jpg",
00587                 "$base/local-thumb/3/3a/Foobar.jpg/220px-Foobar.jpg",
00588                 "$base/local-thumb/3/3a/Foobar.jpg/265px-Foobar.jpg",
00589                 "$base/local-thumb/3/3a/Foobar.jpg/270px-Foobar.jpg",
00590                 "$base/local-thumb/3/3a/Foobar.jpg/274px-Foobar.jpg",
00591                 "$base/local-thumb/3/3a/Foobar.jpg/300px-Foobar.jpg",
00592                 "$base/local-thumb/3/3a/Foobar.jpg/30px-Foobar.jpg",
00593                 "$base/local-thumb/3/3a/Foobar.jpg/330px-Foobar.jpg",
00594                 "$base/local-thumb/3/3a/Foobar.jpg/353px-Foobar.jpg",
00595                 "$base/local-thumb/3/3a/Foobar.jpg/360px-Foobar.jpg",
00596                 "$base/local-thumb/3/3a/Foobar.jpg/400px-Foobar.jpg",
00597                 "$base/local-thumb/3/3a/Foobar.jpg/40px-Foobar.jpg",
00598                 "$base/local-thumb/3/3a/Foobar.jpg/440px-Foobar.jpg",
00599                 "$base/local-thumb/3/3a/Foobar.jpg/442px-Foobar.jpg",
00600                 "$base/local-thumb/3/3a/Foobar.jpg/450px-Foobar.jpg",
00601                 "$base/local-thumb/3/3a/Foobar.jpg/50px-Foobar.jpg",
00602                 "$base/local-thumb/3/3a/Foobar.jpg/600px-Foobar.jpg",
00603                 "$base/local-thumb/3/3a/Foobar.jpg/640px-Foobar.jpg",
00604                 "$base/local-thumb/3/3a/Foobar.jpg/70px-Foobar.jpg",
00605                 "$base/local-thumb/3/3a/Foobar.jpg/75px-Foobar.jpg",
00606                 "$base/local-thumb/3/3a/Foobar.jpg/960px-Foobar.jpg",
00607 
00608                 "$base/local-public/e/ea/Thumb.png",
00609 
00610                 "$base/local-public/0/09/Bad.jpg",
00611 
00612                 "$base/local-public/5/5f/LoremIpsum.djvu",
00613                 "$base/local-thumb/5/5f/LoremIpsum.djvu/page2-2480px-LoremIpsum.djvu.jpg",
00614                 "$base/local-thumb/5/5f/LoremIpsum.djvu/page2-3720px-LoremIpsum.djvu.jpg",
00615                 "$base/local-thumb/5/5f/LoremIpsum.djvu/page2-4960px-LoremIpsum.djvu.jpg",
00616 
00617                 "$base/local-public/f/ff/Foobar.svg",
00618                 "$base/local-thumb/f/ff/Foobar.svg/180px-Foobar.svg.png",
00619                 "$base/local-thumb/f/ff/Foobar.svg/2000px-Foobar.svg.png",
00620                 "$base/local-thumb/f/ff/Foobar.svg/270px-Foobar.svg.png",
00621                 "$base/local-thumb/f/ff/Foobar.svg/3000px-Foobar.svg.png",
00622                 "$base/local-thumb/f/ff/Foobar.svg/360px-Foobar.svg.png",
00623                 "$base/local-thumb/f/ff/Foobar.svg/4000px-Foobar.svg.png",
00624                 "$base/local-thumb/f/ff/Foobar.svg/langde-180px-Foobar.svg.png",
00625                 "$base/local-thumb/f/ff/Foobar.svg/langde-270px-Foobar.svg.png",
00626                 "$base/local-thumb/f/ff/Foobar.svg/langde-360px-Foobar.svg.png",
00627 
00628                 "$base/local-public/math/f/a/5/fa50b8b616463173474302ca3e63586b.png",
00629             )
00630         );
00631     }
00632 
00637     private static function deleteFiles( $files ) {
00638         $backend = RepoGroup::singleton()->getLocalRepo()->getBackend();
00639         foreach ( $files as $file ) {
00640             $backend->delete( array( 'src' => $file ), array( 'force' => 1 ) );
00641         }
00642         foreach ( $files as $file ) {
00643             $tmp = $file;
00644             while ( $tmp = FileBackend::parentStoragePath( $tmp ) ) {
00645                 if ( !$backend->clean( array( 'dir' => $tmp ) )->isOK() ) {
00646                     break;
00647                 }
00648             }
00649         }
00650     }
00651 
00652     protected function getBaseDir() {
00653         return 'mwstore://local-backend';
00654     }
00655 
00656     public function parserTestProvider() {
00657         if ( $this->file === false ) {
00658             global $wgParserTestFiles;
00659             $this->file = $wgParserTestFiles[0];
00660         }
00661 
00662         return new TestFileIterator( $this->file, $this );
00663     }
00664 
00669     public function setParserTestFile( $filename ) {
00670         $this->file = $filename;
00671     }
00672 
00682     public function testParserTest( $desc, $input, $result, $opts, $config ) {
00683         if ( $this->regex != '' && !preg_match( '/' . $this->regex . '/', $desc ) ) {
00684             $this->assertTrue( true ); // XXX: don't flood output with "test made no assertions"
00685             //$this->markTestSkipped( 'Filtered out by the user' );
00686             return;
00687         }
00688 
00689         if ( !$this->isWikitextNS( NS_MAIN ) ) {
00690             // parser tests frequently assume that the main namespace contains wikitext.
00691             // @todo When setting up pages, force the content model. Only skip if
00692             //        $wgtContentModelUseDB is false.
00693             $this->markTestSkipped( "Main namespace does not support wikitext,"
00694                 . "skipping parser test: $desc" );
00695         }
00696 
00697         wfDebug( "Running parser test: $desc\n" );
00698 
00699         $opts = $this->parseOptions( $opts );
00700         $context = $this->setupGlobals( $opts, $config );
00701 
00702         $user = $context->getUser();
00703         $options = ParserOptions::newFromContext( $context );
00704 
00705         if ( isset( $opts['title'] ) ) {
00706             $titleText = $opts['title'];
00707         } else {
00708             $titleText = 'Parser test';
00709         }
00710 
00711         $local = isset( $opts['local'] );
00712         $preprocessor = isset( $opts['preprocessor'] ) ? $opts['preprocessor'] : null;
00713         $parser = $this->getParser( $preprocessor );
00714 
00715         $title = Title::newFromText( $titleText );
00716 
00717         # Parser test requiring math. Make sure texvc is executable
00718         # or just skip such tests.
00719         if ( isset( $opts['math'] ) || isset( $opts['texvc'] ) ) {
00720             global $wgTexvc;
00721 
00722             if ( !isset( $wgTexvc ) ) {
00723                 $this->markTestSkipped( "SKIPPED: \$wgTexvc is not set" );
00724             } elseif ( !is_executable( $wgTexvc ) ) {
00725                 $this->markTestSkipped( "SKIPPED: texvc binary does not exist"
00726                     . " or is not executable.\n"
00727                     . "Current configuration is:\n\$wgTexvc = '$wgTexvc'" );
00728             }
00729         }
00730         if ( isset( $opts['djvu'] ) ) {
00731             if ( !$this->djVuSupport->isEnabled() ) {
00732                 $this->markTestSkipped( "SKIPPED: djvu binaries do not exist or are not executable.\n" );
00733             }
00734         }
00735 
00736         if ( isset( $opts['pst'] ) ) {
00737             $out = $parser->preSaveTransform( $input, $title, $user, $options );
00738         } elseif ( isset( $opts['msg'] ) ) {
00739             $out = $parser->transformMsg( $input, $options, $title );
00740         } elseif ( isset( $opts['section'] ) ) {
00741             $section = $opts['section'];
00742             $out = $parser->getSection( $input, $section );
00743         } elseif ( isset( $opts['replace'] ) ) {
00744             $section = $opts['replace'][0];
00745             $replace = $opts['replace'][1];
00746             $out = $parser->replaceSection( $input, $section, $replace );
00747         } elseif ( isset( $opts['comment'] ) ) {
00748             $out = Linker::formatComment( $input, $title, $local );
00749         } elseif ( isset( $opts['preload'] ) ) {
00750             $out = $parser->getPreloadText( $input, $title, $options );
00751         } else {
00752             $output = $parser->parse( $input, $title, $options, true, true, 1337 );
00753             $output->setTOCEnabled( !isset( $opts['notoc'] ) );
00754             $out = $output->getText();
00755             if ( isset( $opts['tidy'] ) ) {
00756                 if ( !$this->tidySupport->isEnabled() ) {
00757                     $this->markTestSkipped( "SKIPPED: tidy extension is not installed.\n" );
00758                 } else {
00759                     $out = MWTidy::tidy( $out );
00760                     $out = preg_replace( '/\s+$/', '', $out );
00761                 }
00762             }
00763 
00764             if ( isset( $opts['showtitle'] ) ) {
00765                 if ( $output->getTitleText() ) {
00766                     $title = $output->getTitleText();
00767                 }
00768 
00769                 $out = "$title\n$out";
00770             }
00771 
00772             if ( isset( $opts['ill'] ) ) {
00773                 $out = implode( ' ', $output->getLanguageLinks() );
00774             } elseif ( isset( $opts['cat'] ) ) {
00775                 $outputPage = $context->getOutput();
00776                 $outputPage->addCategoryLinks( $output->getCategories() );
00777                 $cats = $outputPage->getCategoryLinks();
00778 
00779                 if ( isset( $cats['normal'] ) ) {
00780                     $out = implode( ' ', $cats['normal'] );
00781                 } else {
00782                     $out = '';
00783                 }
00784             }
00785             $parser->mPreprocessor = null;
00786         }
00787 
00788         $this->teardownGlobals();
00789 
00790         $this->assertEquals( $result, $out, $desc );
00791     }
00792 
00801     public function testFuzzTests() {
00802         global $wgParserTestFiles;
00803 
00804         $files = $wgParserTestFiles;
00805 
00806         if ( $this->getCliArg( 'file' ) ) {
00807             $files = array( $this->getCliArg( 'file' ) );
00808         }
00809 
00810         $dict = $this->getFuzzInput( $files );
00811         $dictSize = strlen( $dict );
00812         $logMaxLength = log( $this->maxFuzzTestLength );
00813 
00814         ini_set( 'memory_limit', $this->memoryLimit * 1048576 );
00815 
00816         $user = new User;
00817         $opts = ParserOptions::newFromUser( $user );
00818         $title = Title::makeTitle( NS_MAIN, 'Parser_test' );
00819 
00820         $id = 1;
00821 
00822         while ( true ) {
00823 
00824             // Generate test input
00825             mt_srand( ++$this->fuzzSeed );
00826             $totalLength = mt_rand( 1, $this->maxFuzzTestLength );
00827             $input = '';
00828 
00829             while ( strlen( $input ) < $totalLength ) {
00830                 $logHairLength = mt_rand( 0, 1000000 ) / 1000000 * $logMaxLength;
00831                 $hairLength = min( intval( exp( $logHairLength ) ), $dictSize );
00832                 $offset = mt_rand( 0, $dictSize - $hairLength );
00833                 $input .= substr( $dict, $offset, $hairLength );
00834             }
00835 
00836             $this->setupGlobals();
00837             $parser = $this->getParser();
00838 
00839             // Run the test
00840             try {
00841                 $parser->parse( $input, $title, $opts );
00842                 $this->assertTrue( true, "Test $id, fuzz seed {$this->fuzzSeed}" );
00843             } catch ( Exception $exception ) {
00844                 $input_dump = sprintf( "string(%d) \"%s\"\n", strlen( $input ), $input );
00845 
00846                 $this->assertTrue( false, "Test $id, fuzz seed {$this->fuzzSeed}. \n\n" .
00847                     "Input: $input_dump\n\nError: {$exception->getMessage()}\n\n" .
00848                     "Backtrace: {$exception->getTraceAsString()}" );
00849             }
00850 
00851             $this->teardownGlobals();
00852             $parser->__destruct();
00853 
00854             if ( $id % 100 == 0 ) {
00855                 $usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 );
00856                 //echo "{$this->fuzzSeed}: $numSuccess/$numTotal (mem: $usage%)\n";
00857                 if ( $usage > 90 ) {
00858                     $ret = "Out of memory:\n";
00859                     $memStats = $this->getMemoryBreakdown();
00860 
00861                     foreach ( $memStats as $name => $usage ) {
00862                         $ret .= "$name: $usage\n";
00863                     }
00864 
00865                     throw new MWException( $ret );
00866                 }
00867             }
00868 
00869             $id++;
00870         }
00871     }
00872 
00873     //Various getter functions
00874 
00880     function getFuzzInput( $filenames ) {
00881         $dict = '';
00882 
00883         foreach ( $filenames as $filename ) {
00884             $contents = file_get_contents( $filename );
00885             preg_match_all( '/!!\s*input\n(.*?)\n!!\s*result/s', $contents, $matches );
00886 
00887             foreach ( $matches[1] as $match ) {
00888                 $dict .= $match . "\n";
00889             }
00890         }
00891 
00892         return $dict;
00893     }
00894 
00899     function getMemoryBreakdown() {
00900         $memStats = array();
00901 
00902         foreach ( $GLOBALS as $name => $value ) {
00903             $memStats['$' . $name] = strlen( serialize( $value ) );
00904         }
00905 
00906         $classes = get_declared_classes();
00907 
00908         foreach ( $classes as $class ) {
00909             $rc = new ReflectionClass( $class );
00910             $props = $rc->getStaticProperties();
00911             $memStats[$class] = strlen( serialize( $props ) );
00912             $methods = $rc->getMethods();
00913 
00914             foreach ( $methods as $method ) {
00915                 $memStats[$class] += strlen( serialize( $method->getStaticVariables() ) );
00916             }
00917         }
00918 
00919         $functions = get_defined_functions();
00920 
00921         foreach ( $functions['user'] as $function ) {
00922             $rf = new ReflectionFunction( $function );
00923             $memStats["$function()"] = strlen( serialize( $rf->getStaticVariables() ) );
00924         }
00925 
00926         asort( $memStats );
00927 
00928         return $memStats;
00929     }
00930 
00936     function getParser( $preprocessor = null ) {
00937         global $wgParserConf;
00938 
00939         $class = $wgParserConf['class'];
00940         $parser = new $class( array( 'preprocessorClass' => $preprocessor ) + $wgParserConf );
00941 
00942         wfRunHooks( 'ParserTestParser', array( &$parser ) );
00943 
00944         return $parser;
00945     }
00946 
00947     //Various action functions
00948 
00949     public function addArticle( $name, $text, $line ) {
00950         self::$articles[$name] = array( $text, $line );
00951     }
00952 
00953     public function publishTestArticles() {
00954         if ( empty( self::$articles ) ) {
00955             return;
00956         }
00957 
00958         foreach ( self::$articles as $name => $info ) {
00959             list( $text, $line ) = $info;
00960             ParserTest::addArticle( $name, $text, $line, 'ignoreduplicate' );
00961         }
00962     }
00963 
00972     public function requireHook( $name ) {
00973         global $wgParser;
00974         $wgParser->firstCallInit(); // make sure hooks are loaded.
00975         return isset( $wgParser->mTagHooks[$name] );
00976     }
00977 
00978     public function requireFunctionHook( $name ) {
00979         global $wgParser;
00980         $wgParser->firstCallInit(); // make sure hooks are loaded.
00981         return isset( $wgParser->mFunctionHooks[$name] );
00982     }
00983 
00984     public function requireTransparentHook( $name ) {
00985         global $wgParser;
00986         $wgParser->firstCallInit(); // make sure hooks are loaded.
00987         return isset( $wgParser->mTransparentTagHooks[$name] );
00988     }
00989 
00990     //Various "cleanup" functions
00991 
00997     public function removeEndingNewline( $s ) {
00998         if ( substr( $s, -1 ) === "\n" ) {
00999             return substr( $s, 0, -1 );
01000         } else {
01001             return $s;
01002         }
01003     }
01004 
01005     //Test options parser functions
01006 
01007     protected function parseOptions( $instring ) {
01008         $opts = array();
01009         // foo
01010         // foo=bar
01011         // foo="bar baz"
01012         // foo=[[bar baz]]
01013         // foo=bar,"baz quux"
01014         $regex = '/\b
01015             ([\w-]+)                        # Key
01016             \b
01017             (?:\s*
01018                 =                       # First sub-value
01019                 \s*
01020                 (
01021                     "
01022                         [^"]*           # Quoted val
01023                     "
01024                 |
01025                     \[\[
01026                         [^]]*           # Link target
01027                     \]\]
01028                 |
01029                     [\w-]+              # Plain word
01030                 )
01031                 (?:\s*
01032                     ,                   # Sub-vals 1..N
01033                     \s*
01034                     (
01035                         "[^"]*"         # Quoted val
01036                     |
01037                         \[\[[^]]*\]\]   # Link target
01038                     |
01039                         [\w-]+          # Plain word
01040                     )
01041                 )*
01042             )?
01043             /x';
01044 
01045         if ( preg_match_all( $regex, $instring, $matches, PREG_SET_ORDER ) ) {
01046             foreach ( $matches as $bits ) {
01047                 array_shift( $bits );
01048                 $key = strtolower( array_shift( $bits ) );
01049                 if ( count( $bits ) == 0 ) {
01050                     $opts[$key] = true;
01051                 } elseif ( count( $bits ) == 1 ) {
01052                     $opts[$key] = $this->cleanupOption( array_shift( $bits ) );
01053                 } else {
01054                     // Array!
01055                     $opts[$key] = array_map( array( $this, 'cleanupOption' ), $bits );
01056                 }
01057             }
01058         }
01059 
01060         return $opts;
01061     }
01062 
01063     protected function cleanupOption( $opt ) {
01064         if ( substr( $opt, 0, 1 ) == '"' ) {
01065             return substr( $opt, 1, -1 );
01066         }
01067 
01068         if ( substr( $opt, 0, 2 ) == '[[' ) {
01069             return substr( $opt, 2, -2 );
01070         }
01071 
01072         return $opt;
01073     }
01074 
01082     protected static function getOptionValue( $key, $opts, $default ) {
01083         $key = strtolower( $key );
01084 
01085         if ( isset( $opts[$key] ) ) {
01086             return $opts[$key];
01087         } else {
01088             return $default;
01089         }
01090     }
01091 }