MediaWiki  master
parserTest.inc
Go to the documentation of this file.
00001 <?php
00032 class ParserTest {
00036         private $color;
00037 
00041         private $showOutput;
00042 
00046         private $useTemporaryTables = true;
00047 
00051         private $databaseSetupDone = false;
00052 
00057         private $db;
00058 
00063         private $dbClone;
00064 
00068         private $oldTablePrefix;
00069 
00070         private $maxFuzzTestLength = 300;
00071         private $fuzzSeed = 0;
00072         private $memoryLimit = 50;
00073         private $uploadDir = null;
00074 
00075         public $regex = "";
00076         private $savedGlobals = array();
00081         public function __construct( $options = array() ) {
00082                 # Only colorize output if stdout is a terminal.
00083                 $this->color = !wfIsWindows() && Maintenance::posix_isatty( 1 );
00084 
00085                 if ( isset( $options['color'] ) ) {
00086                         switch( $options['color'] ) {
00087                         case 'no':
00088                                 $this->color = false;
00089                                 break;
00090                         case 'yes':
00091                         default:
00092                                 $this->color = true;
00093                                 break;
00094                         }
00095                 }
00096 
00097                 $this->term = $this->color
00098                         ? new AnsiTermColorer()
00099                         : new DummyTermColorer();
00100 
00101                 $this->showDiffs = !isset( $options['quick'] );
00102                 $this->showProgress = !isset( $options['quiet'] );
00103                 $this->showFailure = !(
00104                         isset( $options['quiet'] )
00105                         && ( isset( $options['record'] )
00106                                 || isset( $options['compare'] ) ) ); // redundant output
00107 
00108                 $this->showOutput = isset( $options['show-output'] );
00109 
00110                 if ( isset( $options['filter'] ) ) {
00111                         $options['regex'] = $options['filter'];
00112                 }
00113 
00114                 if ( isset( $options['regex'] ) ) {
00115                         if ( isset( $options['record'] ) ) {
00116                                 echo "Warning: --record cannot be used with --regex, disabling --record\n";
00117                                 unset( $options['record'] );
00118                         }
00119                         $this->regex = $options['regex'];
00120                 } else {
00121                         # Matches anything
00122                         $this->regex = '';
00123                 }
00124 
00125                 $this->setupRecorder( $options );
00126                 $this->keepUploads = isset( $options['keep-uploads'] );
00127 
00128                 if ( isset( $options['seed'] ) ) {
00129                         $this->fuzzSeed = intval( $options['seed'] ) - 1;
00130                 }
00131 
00132                 $this->runDisabled = isset( $options['run-disabled'] );
00133 
00134                 $this->hooks = array();
00135                 $this->functionHooks = array();
00136                 self::setUp();
00137         }
00138 
00139         static function setUp() {
00140                 global $wgParser, $wgParserConf, $IP, $messageMemc, $wgMemc,
00141                         $wgUser, $wgLang, $wgOut, $wgRequest, $wgStyleDirectory, $wgEnableParserCache,
00142                         $wgNamespaceAliases, $wgNamespaceProtection, $wgLocalFileRepo,
00143                         $parserMemc, $wgThumbnailScriptPath, $wgScriptPath,
00144                         $wgArticlePath, $wgStyleSheetPath, $wgScript, $wgStylePath, $wgExtensionAssetsPath,
00145                         $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType, $wgLockManagers;
00146 
00147                 $wgScript = '/index.php';
00148                 $wgScriptPath = '/';
00149                 $wgArticlePath = '/wiki/$1';
00150                 $wgStyleSheetPath = '/skins';
00151                 $wgStylePath = '/skins';
00152                 $wgExtensionAssetsPath = '/extensions';
00153                 $wgThumbnailScriptPath = false;
00154                 $wgLockManagers = array( array(
00155                         'name'          => 'fsLockManager',
00156                         'class'         => 'FSLockManager',
00157                         'lockDirectory' => wfTempDir() . '/test-repo/lockdir',
00158                 ) );
00159                 $wgLocalFileRepo = array(
00160                         'class'           => 'LocalRepo',
00161                         'name'            => 'local',
00162                         'url'             => 'http://example.com/images',
00163                         'hashLevels'      => 2,
00164                         'transformVia404' => false,
00165                         'backend'         => new FSFileBackend( array(
00166                                 'name'        => 'local-backend',
00167                                 'lockManager' => 'fsLockManager',
00168                                 'containerPaths' => array(
00169                                         'local-public'  => wfTempDir() . '/test-repo/public',
00170                                         'local-thumb'   => wfTempDir() . '/test-repo/thumb',
00171                                         'local-temp'    => wfTempDir() . '/test-repo/temp',
00172                                         'local-deleted' => wfTempDir() . '/test-repo/deleted',
00173                                 )
00174                         ) )
00175                 );
00176                 $wgNamespaceProtection[NS_MEDIAWIKI] = 'editinterface';
00177                 $wgNamespaceAliases['Image'] = NS_FILE;
00178                 $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK;
00179 
00180                 // XXX: tests won't run without this (for CACHE_DB)
00181                 if ( $wgMainCacheType === CACHE_DB ) {
00182                         $wgMainCacheType = CACHE_NONE;
00183                 }
00184                 if ( $wgMessageCacheType === CACHE_DB ) {
00185                         $wgMessageCacheType = CACHE_NONE;
00186                 }
00187                 if ( $wgParserCacheType === CACHE_DB ) {
00188                         $wgParserCacheType = CACHE_NONE;
00189                 }
00190 
00191                 $wgEnableParserCache = false;
00192                 DeferredUpdates::clearPendingUpdates();
00193                 $wgMemc = wfGetMainCache(); // checks $wgMainCacheType
00194                 $messageMemc = wfGetMessageCacheStorage();
00195                 $parserMemc = wfGetParserCacheStorage();
00196 
00197                 // $wgContLang = new StubContLang;
00198                 $wgUser = new User;
00199                 $context = new RequestContext;
00200                 $wgLang = $context->getLanguage();
00201                 $wgOut = $context->getOutput();
00202                 $wgParser = new StubObject( 'wgParser', $wgParserConf['class'], array( $wgParserConf ) );
00203                 $wgRequest = $context->getRequest();
00204 
00205                 if ( $wgStyleDirectory === false ) {
00206                         $wgStyleDirectory   = "$IP/skins";
00207                 }
00208 
00209         }
00210 
00211         public function setupRecorder ( $options ) {
00212                 if ( isset( $options['record'] ) ) {
00213                         $this->recorder = new DbTestRecorder( $this );
00214                         $this->recorder->version = isset( $options['setversion'] ) ?
00215                                         $options['setversion'] : SpecialVersion::getVersion();
00216                 } elseif ( isset( $options['compare'] ) ) {
00217                         $this->recorder = new DbTestPreviewer( $this );
00218                 } else {
00219                         $this->recorder = new TestRecorder( $this );
00220                 }
00221         }
00222 
00227         static public function chomp( $s ) {
00228                 if ( substr( $s, -1 ) === "\n" ) {
00229                         return substr( $s, 0, -1 );
00230                 }
00231                 else {
00232                         return $s;
00233                 }
00234         }
00235 
00240         function fuzzTest( $filenames ) {
00241                 $GLOBALS['wgContLang'] = Language::factory( 'en' );
00242                 $dict = $this->getFuzzInput( $filenames );
00243                 $dictSize = strlen( $dict );
00244                 $logMaxLength = log( $this->maxFuzzTestLength );
00245                 $this->setupDatabase();
00246                 ini_set( 'memory_limit', $this->memoryLimit * 1048576 );
00247 
00248                 $numTotal = 0;
00249                 $numSuccess = 0;
00250                 $user = new User;
00251                 $opts = ParserOptions::newFromUser( $user );
00252                 $title = Title::makeTitle( NS_MAIN, 'Parser_test' );
00253 
00254                 while ( true ) {
00255                         // Generate test input
00256                         mt_srand( ++$this->fuzzSeed );
00257                         $totalLength = mt_rand( 1, $this->maxFuzzTestLength );
00258                         $input = '';
00259 
00260                         while ( strlen( $input ) < $totalLength ) {
00261                                 $logHairLength = mt_rand( 0, 1000000 ) / 1000000 * $logMaxLength;
00262                                 $hairLength = min( intval( exp( $logHairLength ) ), $dictSize );
00263                                 $offset = mt_rand( 0, $dictSize - $hairLength );
00264                                 $input .= substr( $dict, $offset, $hairLength );
00265                         }
00266 
00267                         $this->setupGlobals();
00268                         $parser = $this->getParser();
00269 
00270                         // Run the test
00271                         try {
00272                                 $parser->parse( $input, $title, $opts );
00273                                 $fail = false;
00274                         } catch ( Exception $exception ) {
00275                                 $fail = true;
00276                         }
00277 
00278                         if ( $fail ) {
00279                                 echo "Test failed with seed {$this->fuzzSeed}\n";
00280                                 echo "Input:\n";
00281                                 printf( "string(%d) \"%s\"\n\n", strlen( $input ), $input );
00282                                 echo "$exception\n";
00283                         } else {
00284                                 $numSuccess++;
00285                         }
00286 
00287                         $numTotal++;
00288                         $this->teardownGlobals();
00289                         $parser->__destruct();
00290 
00291                         if ( $numTotal % 100 == 0 ) {
00292                                 $usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 );
00293                                 echo "{$this->fuzzSeed}: $numSuccess/$numTotal (mem: $usage%)\n";
00294                                 if ( $usage > 90 ) {
00295                                         echo "Out of memory:\n";
00296                                         $memStats = $this->getMemoryBreakdown();
00297 
00298                                         foreach ( $memStats as $name => $usage ) {
00299                                                 echo "$name: $usage\n";
00300                                         }
00301                                         $this->abort();
00302                                 }
00303                         }
00304                 }
00305         }
00306 
00310         function getFuzzInput( $filenames ) {
00311                 $dict = '';
00312 
00313                 foreach ( $filenames as $filename ) {
00314                         $contents = file_get_contents( $filename );
00315                         preg_match_all( '/!!\s*input\n(.*?)\n!!\s*result/s', $contents, $matches );
00316 
00317                         foreach ( $matches[1] as $match ) {
00318                                 $dict .= $match . "\n";
00319                         }
00320                 }
00321 
00322                 return $dict;
00323         }
00324 
00328         function getMemoryBreakdown() {
00329                 $memStats = array();
00330 
00331                 foreach ( $GLOBALS as $name => $value ) {
00332                         $memStats['$' . $name] = strlen( serialize( $value ) );
00333                 }
00334 
00335                 $classes = get_declared_classes();
00336 
00337                 foreach ( $classes as $class ) {
00338                         $rc = new ReflectionClass( $class );
00339                         $props = $rc->getStaticProperties();
00340                         $memStats[$class] = strlen( serialize( $props ) );
00341                         $methods = $rc->getMethods();
00342 
00343                         foreach ( $methods as $method ) {
00344                                 $memStats[$class] += strlen( serialize( $method->getStaticVariables() ) );
00345                         }
00346                 }
00347 
00348                 $functions = get_defined_functions();
00349 
00350                 foreach ( $functions['user'] as $function ) {
00351                         $rf = new ReflectionFunction( $function );
00352                         $memStats["$function()"] = strlen( serialize( $rf->getStaticVariables() ) );
00353                 }
00354 
00355                 asort( $memStats );
00356 
00357                 return $memStats;
00358         }
00359 
00360         function abort() {
00361                 $this->abort();
00362         }
00363 
00375         public function runTestsFromFiles( $filenames ) {
00376                 $ok = false;
00377                 $GLOBALS['wgContLang'] = Language::factory( 'en' );
00378                 $this->recorder->start();
00379                 try {
00380                         $this->setupDatabase();
00381                         $ok = true;
00382 
00383                         foreach ( $filenames as $filename ) {
00384                                 $tests = new TestFileIterator( $filename, $this );
00385                                 $ok = $this->runTests( $tests ) && $ok;
00386                         }
00387 
00388                         $this->teardownDatabase();
00389                         $this->recorder->report();
00390                 } catch (DBError $e) {
00391                         echo $e->getMessage();
00392                 }
00393                 $this->recorder->end();
00394 
00395                 return $ok;
00396         }
00397 
00398         function runTests( $tests ) {
00399                 $ok = true;
00400 
00401                 foreach ( $tests as $t ) {
00402                         $result =
00403                                 $this->runTest( $t['test'], $t['input'], $t['result'], $t['options'], $t['config'] );
00404                         $ok = $ok && $result;
00405                         $this->recorder->record( $t['test'], $result );
00406                 }
00407 
00408                 if ( $this->showProgress ) {
00409                         print "\n";
00410                 }
00411 
00412                 return $ok;
00413         }
00414 
00418         function getParser( $preprocessor = null ) {
00419                 global $wgParserConf;
00420 
00421                 $class = $wgParserConf['class'];
00422                 $parser = new $class( array( 'preprocessorClass' => $preprocessor ) + $wgParserConf );
00423 
00424                 foreach ( $this->hooks as $tag => $callback ) {
00425                         $parser->setHook( $tag, $callback );
00426                 }
00427 
00428                 foreach ( $this->functionHooks as $tag => $bits ) {
00429                         list( $callback, $flags ) = $bits;
00430                         $parser->setFunctionHook( $tag, $callback, $flags );
00431                 }
00432 
00433                 wfRunHooks( 'ParserTestParser', array( &$parser ) );
00434 
00435                 return $parser;
00436         }
00437 
00450         public function runTest( $desc, $input, $result, $opts, $config ) {
00451                 if ( $this->showProgress ) {
00452                         $this->showTesting( $desc );
00453                 }
00454 
00455                 $opts = $this->parseOptions( $opts );
00456                 $context = $this->setupGlobals( $opts, $config );
00457 
00458                 $user = $context->getUser();
00459                 $options = ParserOptions::newFromContext( $context );
00460 
00461                 if ( isset( $opts['title'] ) ) {
00462                         $titleText = $opts['title'];
00463                 }
00464                 else {
00465                         $titleText = 'Parser test';
00466                 }
00467 
00468                 $local = isset( $opts['local'] );
00469                 $preprocessor = isset( $opts['preprocessor'] ) ? $opts['preprocessor'] : null;
00470                 $parser = $this->getParser( $preprocessor );
00471                 $title = Title::newFromText( $titleText );
00472 
00473                 if ( isset( $opts['pst'] ) ) {
00474                         $out = $parser->preSaveTransform( $input, $title, $user, $options );
00475                 } elseif ( isset( $opts['msg'] ) ) {
00476                         $out = $parser->transformMsg( $input, $options, $title );
00477                 } elseif ( isset( $opts['section'] ) ) {
00478                         $section = $opts['section'];
00479                         $out = $parser->getSection( $input, $section );
00480                 } elseif ( isset( $opts['replace'] ) ) {
00481                         $section = $opts['replace'][0];
00482                         $replace = $opts['replace'][1];
00483                         $out = $parser->replaceSection( $input, $section, $replace );
00484                 } elseif ( isset( $opts['comment'] ) ) {
00485                         $out = Linker::formatComment( $input, $title, $local );
00486                 } elseif ( isset( $opts['preload'] ) ) {
00487                         $out = $parser->getpreloadText( $input, $title, $options );
00488                 } else {
00489                         $output = $parser->parse( $input, $title, $options, true, true, 1337 );
00490                         $out = $output->getText();
00491 
00492                         if ( isset( $opts['showtitle'] ) ) {
00493                                 if ( $output->getTitleText() ) {
00494                                         $title = $output->getTitleText();
00495                                 }
00496 
00497                                 $out = "$title\n$out";
00498                         }
00499 
00500                         if ( isset( $opts['ill'] ) ) {
00501                                 $out = $this->tidy( implode( ' ', $output->getLanguageLinks() ) );
00502                         } elseif ( isset( $opts['cat'] ) ) {
00503                                 $outputPage = $context->getOutput();
00504                                 $outputPage->addCategoryLinks( $output->getCategories() );
00505                                 $cats = $outputPage->getCategoryLinks();
00506 
00507                                 if ( isset( $cats['normal'] ) ) {
00508                                         $out = $this->tidy( implode( ' ', $cats['normal'] ) );
00509                                 } else {
00510                                         $out = '';
00511                                 }
00512                         }
00513 
00514                         $result = $this->tidy( $result );
00515                 }
00516 
00517                 $this->teardownGlobals();
00518                 return $this->showTestResult( $desc, $result, $out );
00519         }
00520 
00524         function showTestResult( $desc, $result, $out ) {
00525                 if ( $result === $out ) {
00526                         $this->showSuccess( $desc );
00527                         return true;
00528                 } else {
00529                         $this->showFailure( $desc, $result, $out );
00530                         return false;
00531                 }
00532         }
00533 
00540         private static function getOptionValue( $key, $opts, $default ) {
00541                 $key = strtolower( $key );
00542 
00543                 if ( isset( $opts[$key] ) ) {
00544                         return $opts[$key];
00545                 } else {
00546                         return $default;
00547                 }
00548         }
00549 
00550         private function parseOptions( $instring ) {
00551                 $opts = array();
00552                 // foo
00553                 // foo=bar
00554                 // foo="bar baz"
00555                 // foo=[[bar baz]]
00556                 // foo=bar,"baz quux"
00557                 $regex = '/\b
00558                         ([\w-]+)                                                # Key
00559                         \b
00560                         (?:\s*
00561                                 =                                               # First sub-value
00562                                 \s*
00563                                 (
00564                                         "
00565                                                 [^"]*                   # Quoted val
00566                                         "
00567                                 |
00568                                         \[\[
00569                                                 [^]]*                   # Link target
00570                                         \]\]
00571                                 |
00572                                         [\w-]+                          # Plain word
00573                                 )
00574                                 (?:\s*
00575                                         ,                                       # Sub-vals 1..N
00576                                         \s*
00577                                         (
00578                                                 "[^"]*"                 # Quoted val
00579                                         |
00580                                                 \[\[[^]]*\]\]   # Link target
00581                                         |
00582                                                 [\w-]+                  # Plain word
00583                                         )
00584                                 )*
00585                         )?
00586                         /x';
00587 
00588                 if ( preg_match_all( $regex, $instring, $matches, PREG_SET_ORDER ) ) {
00589                         foreach ( $matches as $bits ) {
00590                                 array_shift( $bits );
00591                                 $key = strtolower( array_shift( $bits ) );
00592                                 if ( count( $bits ) == 0 ) {
00593                                         $opts[$key] = true;
00594                                 } elseif ( count( $bits ) == 1 ) {
00595                                         $opts[$key] = $this->cleanupOption( array_shift( $bits ) );
00596                                 } else {
00597                                         // Array!
00598                                         $opts[$key] = array_map( array( $this, 'cleanupOption' ), $bits );
00599                                 }
00600                         }
00601                 }
00602                 return $opts;
00603         }
00604 
00605         private function cleanupOption( $opt ) {
00606                 if ( substr( $opt, 0, 1 ) == '"' ) {
00607                         return substr( $opt, 1, -1 );
00608                 }
00609 
00610                 if ( substr( $opt, 0, 2 ) == '[[' ) {
00611                         return substr( $opt, 2, -2 );
00612                 }
00613                 return $opt;
00614         }
00615 
00620         private function setupGlobals( $opts = '', $config = '' ) {
00621                 # Find out values for some special options.
00622                 $lang =
00623                         self::getOptionValue( 'language', $opts, 'en' );
00624                 $variant =
00625                         self::getOptionValue( 'variant', $opts, false );
00626                 $maxtoclevel =
00627                         self::getOptionValue( 'wgMaxTocLevel', $opts, 999 );
00628                 $linkHolderBatchSize =
00629                         self::getOptionValue( 'wgLinkHolderBatchSize', $opts, 1000 );
00630 
00631                 $settings = array(
00632                         'wgServer' => 'http://Britney-Spears',
00633                         'wgScript' => '/index.php',
00634                         'wgScriptPath' => '/',
00635                         'wgArticlePath' => '/wiki/$1',
00636                         'wgActionPaths' => array(),
00637                         'wgLockManagers' => array(
00638                                 'name'          => 'fsLockManager',
00639                                 'class'         => 'FSLockManager',
00640                                 'lockDirectory' => $this->uploadDir . '/lockdir',
00641                         ),
00642                         'wgLocalFileRepo' => array(
00643                                 'class' => 'LocalRepo',
00644                                 'name' => 'local',
00645                                 'url' => 'http://example.com/images',
00646                                 'hashLevels' => 2,
00647                                 'transformVia404' => false,
00648                                 'backend'         => new FSFileBackend( array(
00649                                         'name'        => 'local-backend',
00650                                         'lockManager' => 'fsLockManager',
00651                                         'containerPaths' => array(
00652                                                 'local-public'  => $this->uploadDir,
00653                                                 'local-thumb'   => $this->uploadDir . '/thumb',
00654                                                 'local-temp'    => $this->uploadDir . '/temp',
00655                                                 'local-deleted' => $this->uploadDir . '/delete',
00656                                         )
00657                                 ) )
00658                         ),
00659                         'wgEnableUploads' => self::getOptionValue( 'wgEnableUploads', $opts, true ),
00660                         'wgStylePath' => '/skins',
00661                         'wgStyleSheetPath' => '/skins',
00662                         'wgSitename' => 'MediaWiki',
00663                         'wgLanguageCode' => $lang,
00664                         'wgDBprefix' => $this->db->getType() != 'oracle' ? 'parsertest_' : 'pt_',
00665                         'wgRawHtml' => isset( $opts['rawhtml'] ),
00666                         'wgLang' => null,
00667                         'wgContLang' => null,
00668                         'wgNamespacesWithSubpages' => array( 0 => isset( $opts['subpage'] ) ),
00669                         'wgMaxTocLevel' => $maxtoclevel,
00670                         'wgCapitalLinks' => true,
00671                         'wgNoFollowLinks' => true,
00672                         'wgNoFollowDomainExceptions' => array(),
00673                         'wgThumbnailScriptPath' => false,
00674                         'wgUseImageResize' => true,
00675                         'wgLocaltimezone' => 'UTC',
00676                         'wgAllowExternalImages' => true,
00677                         'wgUseTidy' => false,
00678                         'wgDefaultLanguageVariant' => $variant,
00679                         'wgVariantArticlePath' => false,
00680                         'wgGroupPermissions' => array( '*' => array(
00681                                 'createaccount' => true,
00682                                 'read'          => true,
00683                                 'edit'          => true,
00684                                 'createpage'    => true,
00685                                 'createtalk'    => true,
00686                         ) ),
00687                         'wgNamespaceProtection' => array( NS_MEDIAWIKI => 'editinterface' ),
00688                         'wgDefaultExternalStore' => array(),
00689                         'wgForeignFileRepos' => array(),
00690                         'wgLinkHolderBatchSize' => $linkHolderBatchSize,
00691                         'wgExperimentalHtmlIds' => false,
00692                         'wgExternalLinkTarget' => false,
00693                         'wgAlwaysUseTidy' => false,
00694                         'wgHtml5' => true,
00695                         'wgCleanupPresentationalAttributes' => true,
00696                         'wgWellFormedXml' => true,
00697                         'wgAllowMicrodataAttributes' => true,
00698                         'wgAdaptiveMessageCache' => true,
00699                         'wgDisableLangConversion' => false,
00700                         'wgDisableTitleConversion' => false,
00701                 );
00702 
00703                 if ( $config ) {
00704                         $configLines = explode( "\n", $config );
00705 
00706                         foreach ( $configLines as $line ) {
00707                                 list( $var, $value ) = explode( '=', $line, 2 );
00708 
00709                                 $settings[$var] = eval( "return $value;" );
00710                         }
00711                 }
00712 
00713                 $this->savedGlobals = array();
00714 
00716                 wfRunHooks( 'ParserTestGlobals', array( &$settings ) );
00717 
00718                 foreach ( $settings as $var => $val ) {
00719                         if ( array_key_exists( $var, $GLOBALS ) ) {
00720                                 $this->savedGlobals[$var] = $GLOBALS[$var];
00721                         }
00722 
00723                         $GLOBALS[$var] = $val;
00724                 }
00725 
00726                 $GLOBALS['wgContLang'] = Language::factory( $lang );
00727                 $GLOBALS['wgMemc'] = new EmptyBagOStuff;
00728 
00729                 $context = new RequestContext();
00730                 $GLOBALS['wgLang'] = $context->getLanguage();
00731                 $GLOBALS['wgOut'] = $context->getOutput();
00732 
00733                 $GLOBALS['wgUser'] = new User();
00734 
00735                 global $wgHooks;
00736 
00737                 $wgHooks['ParserTestParser'][] = 'ParserTestParserHook::setup';
00738                 $wgHooks['ParserGetVariableValueTs'][] = 'ParserTest::getFakeTimestamp';
00739 
00740                 MagicWord::clearCache();
00741 
00742                 return $context;
00743         }
00744 
00749         private function listTables() {
00750                 $tables = array( 'user', 'user_properties', 'user_former_groups', 'page', 'page_restrictions',
00751                         'protected_titles', 'revision', 'text', 'pagelinks', 'imagelinks',
00752                         'categorylinks', 'templatelinks', 'externallinks', 'langlinks', 'iwlinks',
00753                         'site_stats', 'hitcounter',     'ipblocks', 'image', 'oldimage',
00754                         'recentchanges', 'watchlist', 'interwiki', 'logging',
00755                         'querycache', 'objectcache', 'job', 'l10n_cache', 'redirect', 'querycachetwo',
00756                         'archive', 'user_groups', 'page_props', 'category', 'msg_resource', 'msg_resource_links'
00757                 );
00758 
00759                 if ( in_array( $this->db->getType(), array( 'mysql', 'sqlite', 'oracle' ) ) ) {
00760                         array_push( $tables, 'searchindex' );
00761                 }
00762 
00763                 // Allow extensions to add to the list of tables to duplicate;
00764                 // may be necessary if they hook into page save or other code
00765                 // which will require them while running tests.
00766                 wfRunHooks( 'ParserTestTables', array( &$tables ) );
00767 
00768                 return $tables;
00769         }
00770 
00776         public function setupDatabase() {
00777                 global $wgDBprefix;
00778 
00779                 if ( $this->databaseSetupDone ) {
00780                         return;
00781                 }
00782 
00783                 $this->db = wfGetDB( DB_MASTER );
00784                 $dbType = $this->db->getType();
00785 
00786                 if ( $wgDBprefix === 'parsertest_' || ( $dbType == 'oracle' && $wgDBprefix === 'pt_' ) ) {
00787                         throw new MWException( 'setupDatabase should be called before setupGlobals' );
00788                 }
00789 
00790                 $this->databaseSetupDone = true;
00791                 $this->oldTablePrefix = $wgDBprefix;
00792 
00793                 # SqlBagOStuff broke when using temporary tables on r40209 (bug 15892).
00794                 # It seems to have been fixed since (r55079?), but regressed at some point before r85701.
00795                 # This works around it for now...
00796                 ObjectCache::$instances[CACHE_DB] = new HashBagOStuff;
00797 
00798                 # CREATE TEMPORARY TABLE breaks if there is more than one server
00799                 if ( wfGetLB()->getServerCount() != 1 ) {
00800                         $this->useTemporaryTables = false;
00801                 }
00802 
00803                 $temporary = $this->useTemporaryTables || $dbType == 'postgres';
00804                 $prefix = $dbType != 'oracle' ? 'parsertest_' : 'pt_';
00805 
00806                 $this->dbClone = new CloneDatabase( $this->db, $this->listTables(), $prefix );
00807                 $this->dbClone->useTemporaryTables( $temporary );
00808                 $this->dbClone->cloneTableStructure();
00809 
00810                 if ( $dbType == 'oracle' ) {
00811                         $this->db->query( 'BEGIN FILL_WIKI_INFO; END;' );
00812                         # Insert 0 user to prevent FK violations
00813 
00814                         # Anonymous user
00815                         $this->db->insert( 'user', array(
00816                                 'user_id'         => 0,
00817                                 'user_name'       => 'Anonymous' ) );
00818                 }
00819 
00820                 # Hack: insert a few Wikipedia in-project interwiki prefixes,
00821                 # for testing inter-language links
00822                 $this->db->insert( 'interwiki', array(
00823                         array( 'iw_prefix' => 'wikipedia',
00824                                    'iw_url'    => 'http://en.wikipedia.org/wiki/$1',
00825                                    'iw_api'    => '',
00826                                    'iw_wikiid' => '',
00827                                    'iw_local'  => 0 ),
00828                         array( 'iw_prefix' => 'meatball',
00829                                    'iw_url'    => 'http://www.usemod.com/cgi-bin/mb.pl?$1',
00830                                    'iw_api'    => '',
00831                                    'iw_wikiid' => '',
00832                                    'iw_local'  => 0 ),
00833                         array( 'iw_prefix' => 'zh',
00834                                    'iw_url'    => 'http://zh.wikipedia.org/wiki/$1',
00835                                    'iw_api'    => '',
00836                                    'iw_wikiid' => '',
00837                                    'iw_local'  => 1 ),
00838                         array( 'iw_prefix' => 'es',
00839                                    'iw_url'    => 'http://es.wikipedia.org/wiki/$1',
00840                                    'iw_api'    => '',
00841                                    'iw_wikiid' => '',
00842                                    'iw_local'  => 1 ),
00843                         array( 'iw_prefix' => 'fr',
00844                                    'iw_url'    => 'http://fr.wikipedia.org/wiki/$1',
00845                                    'iw_api'    => '',
00846                                    'iw_wikiid' => '',
00847                                    'iw_local'  => 1 ),
00848                         array( 'iw_prefix' => 'ru',
00849                                    'iw_url'    => 'http://ru.wikipedia.org/wiki/$1',
00850                                    'iw_api'    => '',
00851                                    'iw_wikiid' => '',
00852                                    'iw_local'  => 1 ),
00853                         ) );
00854 
00855                 # Update certain things in site_stats
00856                 $this->db->insert( 'site_stats', array( 'ss_row_id' => 1, 'ss_images' => 2, 'ss_good_articles' => 1 ) );
00857 
00858                 # Reinitialise the LocalisationCache to match the database state
00859                 Language::getLocalisationCache()->unloadAll();
00860 
00861                 # Clear the message cache
00862                 MessageCache::singleton()->clear();
00863 
00864                 $this->uploadDir = $this->setupUploadDir();
00865                 $user = User::createNew( 'WikiSysop' );
00866                 $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.jpg' ) );
00867                 $image->recordUpload2( '', 'Upload of some lame file', 'Some lame file', array(
00868                         'size'        => 12345,
00869                         'width'       => 1941,
00870                         'height'      => 220,
00871                         'bits'        => 24,
00872                         'media_type'  => MEDIATYPE_BITMAP,
00873                         'mime'        => 'image/jpeg',
00874                         'metadata'    => serialize( array() ),
00875                         'sha1'        => wfBaseConvert( '', 16, 36, 31 ),
00876                         'fileExists'  => true
00877                         ), $this->db->timestamp( '20010115123500' ), $user );
00878 
00879                 # This image will be blacklisted in [[MediaWiki:Bad image list]]
00880                 $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Bad.jpg' ) );
00881                 $image->recordUpload2( '', 'zomgnotcensored', 'Borderline image', array(
00882                         'size'        => 12345,
00883                         'width'       => 320,
00884                         'height'      => 240,
00885                         'bits'        => 24,
00886                         'media_type'  => MEDIATYPE_BITMAP,
00887                         'mime'        => 'image/jpeg',
00888                         'metadata'    => serialize( array() ),
00889                         'sha1'        => wfBaseConvert( '', 16, 36, 31 ),
00890                         'fileExists'  => true
00891                         ), $this->db->timestamp( '20010115123500' ), $user );
00892         }
00893 
00894         public function teardownDatabase() {
00895                 if ( !$this->databaseSetupDone ) {
00896                         $this->teardownGlobals();
00897                         return;
00898                 }
00899                 $this->teardownUploadDir( $this->uploadDir );
00900 
00901                 $this->dbClone->destroy();
00902                 $this->databaseSetupDone = false;
00903 
00904                 if ( $this->useTemporaryTables ) {
00905                         if( $this->db->getType() == 'sqlite' ) {
00906                                 # Under SQLite the searchindex table is virtual and need
00907                                 # to be explicitly destroyed. See bug 29912
00908                                 # See also MediaWikiTestCase::destroyDB()
00909                                 wfDebug( __METHOD__ . " explicitly destroying sqlite virtual table parsertest_searchindex\n" );
00910                                 $this->db->query( "DROP TABLE `parsertest_searchindex`" );
00911                         }
00912                         # Don't need to do anything
00913                         $this->teardownGlobals();
00914                         return;
00915                 }
00916 
00917                 $tables = $this->listTables();
00918 
00919                 foreach ( $tables as $table ) {
00920                         $sql = $this->db->getType() == 'oracle' ? "DROP TABLE pt_$table DROP CONSTRAINTS" : "DROP TABLE `parsertest_$table`";
00921                         $this->db->query( $sql );
00922                 }
00923 
00924                 if ( $this->db->getType() == 'oracle' )
00925                         $this->db->query( 'BEGIN FILL_WIKI_INFO; END;' );
00926 
00927                 $this->teardownGlobals();
00928         }
00929 
00936         private function setupUploadDir() {
00937                 global $IP;
00938 
00939                 if ( $this->keepUploads ) {
00940                         $dir = wfTempDir() . '/mwParser-images';
00941 
00942                         if ( is_dir( $dir ) ) {
00943                                 return $dir;
00944                         }
00945                 } else {
00946                         $dir = wfTempDir() . "/mwParser-" . mt_rand() . "-images";
00947                 }
00948 
00949                 // wfDebug( "Creating upload directory $dir\n" );
00950                 if ( file_exists( $dir ) ) {
00951                         wfDebug( "Already exists!\n" );
00952                         return $dir;
00953                 }
00954 
00955                 wfMkdirParents( $dir . '/3/3a', null, __METHOD__ );
00956                 copy( "$IP/skins/monobook/headbg.jpg", "$dir/3/3a/Foobar.jpg" );
00957                 wfMkdirParents( $dir . '/0/09', null, __METHOD__ );
00958                 copy( "$IP/skins/monobook/headbg.jpg", "$dir/0/09/Bad.jpg" );
00959 
00960                 return $dir;
00961         }
00962 
00967         private function teardownGlobals() {
00968                 RepoGroup::destroySingleton();
00969                 FileBackendGroup::destroySingleton();
00970                 LockManagerGroup::destroySingleton();
00971                 LinkCache::singleton()->clear();
00972 
00973                 foreach ( $this->savedGlobals as $var => $val ) {
00974                         $GLOBALS[$var] = $val;
00975                 }
00976         }
00977 
00981         private function teardownUploadDir( $dir ) {
00982                 if ( $this->keepUploads ) {
00983                         return;
00984                 }
00985 
00986                 // delete the files first, then the dirs.
00987                 self::deleteFiles(
00988                         array (
00989                                 "$dir/3/3a/Foobar.jpg",
00990                                 "$dir/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg",
00991                                 "$dir/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg",
00992                                 "$dir/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg",
00993                                 "$dir/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg",
00994                                 "$dir/thumb/3/3a/Foobar.jpg/1280px-Foobar.jpg",
00995                                 "$dir/thumb/3/3a/Foobar.jpg/20px-Foobar.jpg",
00996                                 "$dir/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg",
00997                                 "$dir/thumb/3/3a/Foobar.jpg/300px-Foobar.jpg",
00998                                 "$dir/thumb/3/3a/Foobar.jpg/30px-Foobar.jpg",
00999                                 "$dir/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg",
01000                                 "$dir/thumb/3/3a/Foobar.jpg/400px-Foobar.jpg",
01001                                 "$dir/thumb/3/3a/Foobar.jpg/40px-Foobar.jpg",
01002                                 "$dir/thumb/3/3a/Foobar.jpg/70px-Foobar.jpg",
01003                                 "$dir/thumb/3/3a/Foobar.jpg/960px-Foobar.jpg",
01004 
01005                                 "$dir/0/09/Bad.jpg",
01006 
01007                                 "$dir/math/f/a/5/fa50b8b616463173474302ca3e63586b.png",
01008                         )
01009                 );
01010 
01011                 self::deleteDirs(
01012                         array (
01013                                 "$dir/3/3a",
01014                                 "$dir/3",
01015                                 "$dir/thumb/6/65",
01016                                 "$dir/thumb/6",
01017                                 "$dir/thumb/3/3a/Foobar.jpg",
01018                                 "$dir/thumb/3/3a",
01019                                 "$dir/thumb/3",
01020 
01021                                 "$dir/0/09/",
01022                                 "$dir/0/",
01023                                 "$dir/thumb",
01024                                 "$dir/math/f/a/5",
01025                                 "$dir/math/f/a",
01026                                 "$dir/math/f",
01027                                 "$dir/math",
01028                                 "$dir",
01029                         )
01030                 );
01031         }
01032 
01037         private static function deleteFiles( $files ) {
01038                 foreach ( $files as $file ) {
01039                         if ( file_exists( $file ) ) {
01040                                 unlink( $file );
01041                         }
01042                 }
01043         }
01044 
01049         private static function deleteDirs( $dirs ) {
01050                 foreach ( $dirs as $dir ) {
01051                         if ( is_dir( $dir ) ) {
01052                                 rmdir( $dir );
01053                         }
01054                 }
01055         }
01056 
01060         protected function showTesting( $desc ) {
01061                 print "Running test $desc... ";
01062         }
01063 
01070         protected function showSuccess( $desc ) {
01071                 if ( $this->showProgress ) {
01072                         print $this->term->color( '1;32' ) . 'PASSED' . $this->term->reset() . "\n";
01073                 }
01074 
01075                 return true;
01076         }
01077 
01087         protected function showFailure( $desc, $result, $html ) {
01088                 if ( $this->showFailure ) {
01089                         if ( !$this->showProgress ) {
01090                                 # In quiet mode we didn't show the 'Testing' message before the
01091                                 # test, in case it succeeded. Show it now:
01092                                 $this->showTesting( $desc );
01093                         }
01094 
01095                         print $this->term->color( '31' ) . 'FAILED!' . $this->term->reset() . "\n";
01096 
01097                         if ( $this->showOutput ) {
01098                                 print "--- Expected ---\n$result\n--- Actual ---\n$html\n";
01099                         }
01100 
01101                         if ( $this->showDiffs ) {
01102                                 print $this->quickDiff( $result, $html );
01103                                 if ( !$this->wellFormed( $html ) ) {
01104                                         print "XML error: $this->mXmlError\n";
01105                                 }
01106                         }
01107                 }
01108 
01109                 return false;
01110         }
01111 
01122         protected function quickDiff( $input, $output, $inFileTail = 'expected', $outFileTail = 'actual' ) {
01123                 # Windows, or at least the fc utility, is retarded
01124                 $slash = wfIsWindows() ? '\\' : '/';
01125                 $prefix = wfTempDir() . "{$slash}mwParser-" . mt_rand();
01126 
01127                 $infile = "$prefix-$inFileTail";
01128                 $this->dumpToFile( $input, $infile );
01129 
01130                 $outfile = "$prefix-$outFileTail";
01131                 $this->dumpToFile( $output, $outfile );
01132 
01133                 $shellInfile = wfEscapeShellArg($infile);
01134                 $shellOutfile = wfEscapeShellArg($outfile);
01135 
01136                 global $wgDiff3;
01137                 // we assume that people with diff3 also have usual diff
01138                 $diff = ( wfIsWindows() && !$wgDiff3 )
01139                         ? `fc $shellInfile $shellOutfile`
01140                         : `diff -au $shellInfile $shellOutfile`;
01141                 unlink( $infile );
01142                 unlink( $outfile );
01143 
01144                 return $this->colorDiff( $diff );
01145         }
01146 
01153         private function dumpToFile( $data, $filename ) {
01154                 $file = fopen( $filename, "wt" );
01155                 fwrite( $file, $data . "\n" );
01156                 fclose( $file );
01157         }
01158 
01166         protected function colorDiff( $text ) {
01167                 return preg_replace(
01168                         array( '/^(-.*)$/m', '/^(\+.*)$/m' ),
01169                         array( $this->term->color( 34 ) . '$1' . $this->term->reset(),
01170                                    $this->term->color( 31 ) . '$1' . $this->term->reset() ),
01171                         $text );
01172         }
01173 
01179         public function showRunFile( $path ) {
01180                 print $this->term->color( 1 ) .
01181                         "Reading tests from \"$path\"..." .
01182                         $this->term->reset() .
01183                         "\n";
01184         }
01185 
01193         static public function addArticle( $name, $text, $line = 'unknown', $ignoreDuplicate = '' ) {
01194                 global $wgCapitalLinks;
01195 
01196                 $oldCapitalLinks = $wgCapitalLinks;
01197                 $wgCapitalLinks = true; // We only need this from SetupGlobals() See r70917#c8637
01198 
01199                 $text = self::chomp( $text );
01200                 $name = self::chomp( $name );
01201 
01202                 $title = Title::newFromText( $name );
01203 
01204                 if ( is_null( $title ) ) {
01205                         throw new MWException( "invalid title '$name' at line $line\n" );
01206                 }
01207 
01208                 $page = WikiPage::factory( $title );
01209                 $page->loadPageData( 'fromdbmaster' );
01210 
01211                 if ( $page->exists() ) {
01212                         if ( $ignoreDuplicate == 'ignoreduplicate' ) {
01213                                 return;
01214                         } else {
01215                                 throw new MWException( "duplicate article '$name' at line $line\n" );
01216                         }
01217                 }
01218 
01219                 $page->doEditContent( ContentHandler::makeContent( $text, $title ), '', EDIT_NEW );
01220 
01221                 $wgCapitalLinks = $oldCapitalLinks;
01222         }
01223 
01232         public function requireHook( $name ) {
01233                 global $wgParser;
01234 
01235                 $wgParser->firstCallInit( ); // make sure hooks are loaded.
01236 
01237                 if ( isset( $wgParser->mTagHooks[$name] ) ) {
01238                         $this->hooks[$name] = $wgParser->mTagHooks[$name];
01239                 } else {
01240                         echo "   This test suite requires the '$name' hook extension, skipping.\n";
01241                         return false;
01242                 }
01243 
01244                 return true;
01245         }
01246 
01255         public function requireFunctionHook( $name ) {
01256                 global $wgParser;
01257 
01258                 $wgParser->firstCallInit( ); // make sure hooks are loaded.
01259 
01260                 if ( isset( $wgParser->mFunctionHooks[$name] ) ) {
01261                         $this->functionHooks[$name] = $wgParser->mFunctionHooks[$name];
01262                 } else {
01263                         echo "   This test suite requires the '$name' function hook extension, skipping.\n";
01264                         return false;
01265                 }
01266 
01267                 return true;
01268         }
01269 
01277         private function tidy( $text ) {
01278                 global $wgUseTidy;
01279 
01280                 if ( $wgUseTidy ) {
01281                         $text = MWTidy::tidy( $text );
01282                 }
01283 
01284                 return $text;
01285         }
01286 
01287         private function wellFormed( $text ) {
01288                 $html =
01289                         Sanitizer::hackDocType() .
01290                         '<html>' .
01291                         $text .
01292                         '</html>';
01293 
01294                 $parser = xml_parser_create( "UTF-8" );
01295 
01296                 # case folding violates XML standard, turn it off
01297                 xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false );
01298 
01299                 if ( !xml_parse( $parser, $html, true ) ) {
01300                         $err = xml_error_string( xml_get_error_code( $parser ) );
01301                         $position = xml_get_current_byte_index( $parser );
01302                         $fragment = $this->extractFragment( $html, $position );
01303                         $this->mXmlError = "$err at byte $position:\n$fragment";
01304                         xml_parser_free( $parser );
01305 
01306                         return false;
01307                 }
01308 
01309                 xml_parser_free( $parser );
01310 
01311                 return true;
01312         }
01313 
01314         private function extractFragment( $text, $position ) {
01315                 $start = max( 0, $position - 10 );
01316                 $before = $position - $start;
01317                 $fragment = '...' .
01318                         $this->term->color( 34 ) .
01319                         substr( $text, $start, $before ) .
01320                         $this->term->color( 0 ) .
01321                         $this->term->color( 31 ) .
01322                         $this->term->color( 1 ) .
01323                         substr( $text, $position, 1 ) .
01324                         $this->term->color( 0 ) .
01325                         $this->term->color( 34 ) .
01326                         substr( $text, $position + 1, 9 ) .
01327                         $this->term->color( 0 ) .
01328                         '...';
01329                 $display = str_replace( "\n", ' ', $fragment );
01330                 $caret = '   ' .
01331                         str_repeat( ' ', $before ) .
01332                         $this->term->color( 31 ) .
01333                         '^' .
01334                         $this->term->color( 0 );
01335 
01336                 return "$display\n$caret";
01337         }
01338 
01339         static function getFakeTimestamp( &$parser, &$ts ) {
01340                 $ts = 123;
01341                 return true;
01342         }
01343 }