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