48 private $useTemporaryTables =
true;
53 private $databaseSetupDone =
false;
70 private $oldTablePrefix;
72 private $maxFuzzTestLength = 300;
73 private $fuzzSeed = 0;
74 private $memoryLimit = 50;
75 private $uploadDir =
null;
78 private $savedGlobals =
array();
85 # Only colorize output if stdout is a terminal.
101 ?
new AnsiTermColorer()
105 $this->showProgress = !isset(
$options[
'quiet'] );
106 $this->showFailure = !(
109 || isset(
$options[
'compare'] ) ) );
111 $this->showOutput = isset(
$options[
'show-output'] );
113 if ( isset(
$options[
'filter'] ) ) {
118 if ( isset(
$options[
'record'] ) ) {
119 echo
"Warning: --record cannot be used with --regex, disabling --record\n";
129 $this->keepUploads = isset(
$options[
'keep-uploads'] );
132 $this->fuzzSeed = intval(
$options[
'seed'] ) - 1;
135 $this->runDisabled = isset(
$options[
'run-disabled'] );
136 $this->runParsoid = isset(
$options[
'run-parsoid'] );
139 $this->functionHooks =
array();
143 static function setUp() {
147 $parserMemc, $wgThumbnailScriptPath, $wgScriptPath,
151 $wgScript =
'/index.php';
154 $wgStylePath =
'/skins';
155 $wgExtensionAssetsPath =
'/extensions';
156 $wgThumbnailScriptPath =
false;
158 'name' =>
'fsLockManager',
159 'class' =>
'FSLockManager',
160 'lockDirectory' =>
wfTempDir() .
'/test-repo/lockdir',
162 'name' =>
'nullLockManager',
163 'class' =>
'NullLockManager',
165 $wgLocalFileRepo =
array(
166 'class' =>
'LocalRepo',
168 'url' =>
'http://example.com/images',
170 'transformVia404' =>
false,
172 'name' =>
'local-backend',
173 'wikiId' => wfWikiId(),
174 'containerPaths' =>
array(
175 'local-public' =>
wfTempDir() .
'/test-repo/public',
176 'local-thumb' =>
wfTempDir() .
'/test-repo/thumb',
177 'local-temp' =>
wfTempDir() .
'/test-repo/temp',
178 'local-deleted' =>
wfTempDir() .
'/test-repo/deleted',
185 # add a namespace shadowing a interwiki link, to test
186 # proper precedence when resolving links. (bug 51680)
187 $wgExtraNamespaces[100] =
'MemoryAlpha';
193 if ( $wgMessageCacheType ===
CACHE_DB ) {
196 if ( $wgParserCacheType ===
CACHE_DB ) {
200 $wgEnableParserCache =
false;
210 $wgOut = $context->getOutput();
212 $wgRequest = $context->getRequest();
214 if ( $wgStyleDirectory ===
false ) {
215 $wgStyleDirectory =
"$IP/skins";
218 self::setupInterwikis();
230 public static function setupInterwikis() {
231 # Hack: insert a few Wikipedia in-project interwiki prefixes,
232 # for testing inter-language links
233 Hooks::register(
'InterwikiLoadPrefix',
function ( $prefix, &$iwData ) {
234 static $testInterwikis =
array(
235 'wikipedia' =>
array(
236 'iw_url' =>
'http://en.wikipedia.org/wiki/$1',
241 'iw_url' =>
'http://www.usemod.com/cgi-bin/mb.pl?$1',
245 'memoryalpha' =>
array(
246 'iw_url' =>
'http://www.memory-alpha.org/en/index.php/$1',
251 'iw_url' =>
'http://zh.wikipedia.org/wiki/$1',
256 'iw_url' =>
'http://es.wikipedia.org/wiki/$1',
261 'iw_url' =>
'http://fr.wikipedia.org/wiki/$1',
266 'iw_url' =>
'http://ru.wikipedia.org/wiki/$1',
271 if ( array_key_exists( $prefix, $testInterwikis ) ) {
272 $iwData = $testInterwikis[$prefix];
283 public static function tearDownInterwikis() {
287 public function setupRecorder(
$options ) {
288 if ( isset(
$options[
'record'] ) ) {
290 $this->recorder->version = isset(
$options[
'setversion'] ) ?
292 } elseif ( isset(
$options[
'compare'] ) ) {
303 public static function chomp(
$s ) {
304 if ( substr(
$s, -1 ) ===
"\n" ) {
305 return substr(
$s, 0, -1 );
315 function fuzzTest( $filenames ) {
317 $dict = $this->getFuzzInput( $filenames );
318 $dictSize = strlen( $dict );
319 $logMaxLength = log( $this->maxFuzzTestLength );
320 $this->setupDatabase();
321 ini_set(
'memory_limit', $this->memoryLimit * 1048576 );
331 mt_srand( ++$this->fuzzSeed );
332 $totalLength = mt_rand( 1, $this->maxFuzzTestLength );
335 while ( strlen( $input ) < $totalLength ) {
336 $logHairLength = mt_rand( 0, 1000000 ) / 1000000 * $logMaxLength;
337 $hairLength = min( intval( exp( $logHairLength ) ), $dictSize );
338 $offset = mt_rand( 0, $dictSize - $hairLength );
339 $input .= substr( $dict, $offset, $hairLength );
342 $this->setupGlobals();
349 }
catch ( Exception $exception ) {
354 echo
"Test failed with seed {$this->fuzzSeed}\n";
356 printf(
"string(%d) \"%s\"\n\n", strlen( $input ), $input );
363 $this->teardownGlobals();
366 if ( $numTotal % 100 == 0 ) {
367 $usage = intval( memory_get_usage(
true ) / $this->memoryLimit / 1048576 * 100 );
368 echo
"{$this->fuzzSeed}: $numSuccess/$numTotal (mem: $usage%)\n";
370 echo
"Out of memory:\n";
371 $memStats = $this->getMemoryBreakdown();
373 foreach ( $memStats
as $name => $usage ) {
374 echo
"$name: $usage\n";
385 function getFuzzInput( $filenames ) {
388 foreach ( $filenames
as $filename ) {
389 $contents = file_get_contents( $filename );
390 preg_match_all(
'/!!\s*(input|wikitext)\n(.*?)\n!!\s*(result|html|html\/\*|html\/php)/s', $contents,
$matches );
393 $dict .= $match .
"\n";
403 function getMemoryBreakdown() {
407 $memStats[
'$' .
$name] = strlen( serialize(
$value ) );
410 $classes = get_declared_classes();
412 foreach ( $classes
as $class ) {
413 $rc =
new ReflectionClass( $class );
414 $props = $rc->getStaticProperties();
415 $memStats[$class] = strlen( serialize( $props ) );
416 $methods = $rc->getMethods();
418 foreach ( $methods
as $method ) {
419 $memStats[$class] += strlen( serialize( $method->getStaticVariables() ) );
423 $functions = get_defined_functions();
425 foreach ( $functions[
'user']
as $function ) {
426 $rf =
new ReflectionFunction( $function );
427 $memStats[
"$function()"] = strlen( serialize( $rf->getStaticVariables() ) );
450 public function runTestsFromFiles( $filenames ) {
458 $this->recorder->start();
460 $this->setupDatabase();
463 foreach ( $filenames
as $filename ) {
465 $ok = $this->runTests( $tests ) &&
$ok;
468 $this->teardownDatabase();
469 $this->recorder->report();
471 echo
$e->getMessage();
473 $this->recorder->end();
478 function runTests( $tests ) {
481 foreach ( $tests
as $t ) {
483 $this->runTest(
$t[
'test'],
$t[
'input'],
$t[
'result'],
$t[
'options'],
$t[
'config'] );
485 $this->recorder->record(
$t[
'test'],
$result );
488 if ( $this->showProgress ) {
501 function getParser( $preprocessor =
null ) {
504 $class = $wgParserConf[
'class'];
505 $parser =
new $class(
array(
'preprocessorClass' => $preprocessor ) + $wgParserConf );
507 foreach ( $this->
hooks as $tag => $callback ) {
508 $parser->setHook( $tag, $callback );
511 foreach ( $this->functionHooks
as $tag => $bits ) {
533 public function runTest( $desc, $input,
$result, $opts, $config ) {
534 if ( $this->showProgress ) {
535 $this->showTesting( $desc );
538 $opts = $this->parseOptions( $opts );
539 $context = $this->setupGlobals( $opts, $config );
541 $user = $context->getUser();
544 if ( isset( $opts[
'title'] ) ) {
545 $titleText = $opts[
'title'];
547 $titleText =
'Parser test';
550 $local = isset( $opts[
'local'] );
551 $preprocessor = isset( $opts[
'preprocessor'] ) ? $opts[
'preprocessor'] :
null;
552 $parser = $this->getParser( $preprocessor );
555 if ( isset( $opts[
'pst'] ) ) {
557 } elseif ( isset( $opts[
'msg'] ) ) {
559 } elseif ( isset( $opts[
'section'] ) ) {
562 } elseif ( isset( $opts[
'replace'] ) ) {
564 $replace = $opts[
'replace'][1];
566 } elseif ( isset( $opts[
'comment'] ) ) {
568 } elseif ( isset( $opts[
'preload'] ) ) {
572 $output->setTOCEnabled( !isset( $opts[
'notoc'] ) );
575 if ( isset( $opts[
'showtitle'] ) ) {
576 if (
$output->getTitleText() ) {
580 $out =
"$title\n$out";
583 if ( isset( $opts[
'ill'] ) ) {
584 $out = $this->tidy( implode(
' ',
$output->getLanguageLinks() ) );
585 } elseif ( isset( $opts[
'cat'] ) ) {
586 $outputPage = $context->getOutput();
587 $outputPage->addCategoryLinks(
$output->getCategories() );
588 $cats = $outputPage->getCategoryLinks();
590 if ( isset( $cats[
'normal'] ) ) {
591 $out = $this->tidy( implode(
' ', $cats[
'normal'] ) );
600 $this->teardownGlobals();
603 $testResult->expected =
$result;
604 $testResult->actual =
$out;
606 return $this->showTestResult( $testResult );
614 $this->showSuccess( $testResult );
617 $this->showFailure( $testResult );
628 private static function getOptionValue( $key, $opts, $default ) {
629 $key = strtolower( $key );
631 if ( isset( $opts[$key] ) ) {
638 private function parseOptions( $instring ) {
647 (?<qstr> # Quoted string
649 (?:[^\\\\"] | \\\\.)*
655 [^"{}] | # Not a quoted string or object, or
656 (?&qstr) | # A quoted string, or
657 (?&json) # A json object (recursively)
663 (?&qstr) # Quoted val
671 (?&json) # JSON object
675 $regex =
'/' . $defs .
'\b
691 $valueregex =
'/' . $defs .
'(?&value)/x';
693 if ( preg_match_all( $regex, $instring,
$matches, PREG_SET_ORDER ) ) {
695 $key = strtolower( $bits[
'k' ] );
696 if ( !isset( $bits[
'v' ] ) ) {
699 preg_match_all( $valueregex, $bits[
'v' ], $vmatches );
700 $opts[$key] = array_map(
array( $this,
'cleanupOption' ), $vmatches[0] );
701 if ( count( $opts[$key] ) == 1 ) {
702 $opts[$key] = $opts[$key][0];
710 private function cleanupOption( $opt ) {
711 if ( substr( $opt, 0, 1 ) ==
'"' ) {
712 return stripcslashes( substr( $opt, 1, -1 ) );
715 if ( substr( $opt, 0, 2 ) ==
'[[' ) {
716 return substr( $opt, 2, -2 );
719 if ( substr( $opt, 0, 1 ) ==
'{' ) {
729 private function setupGlobals( $opts =
'', $config =
'' ) {
730 # Find out values for some special options.
732 self::getOptionValue(
'language', $opts,
'en' );
734 self::getOptionValue(
'variant', $opts,
false );
736 self::getOptionValue(
'wgMaxTocLevel', $opts, 999 );
737 $linkHolderBatchSize =
738 self::getOptionValue(
'wgLinkHolderBatchSize', $opts, 1000 );
741 'wgServer' =>
'http://example.org',
742 'wgScript' =>
'/index.php',
743 'wgScriptPath' =>
'/',
744 'wgArticlePath' =>
'/wiki/$1',
745 'wgActionPaths' =>
array(),
747 'name' =>
'fsLockManager',
748 'class' =>
'FSLockManager',
749 'lockDirectory' => $this->uploadDir .
'/lockdir',
751 'name' =>
'nullLockManager',
752 'class' =>
'NullLockManager',
754 'wgLocalFileRepo' =>
array(
755 'class' =>
'LocalRepo',
757 'url' =>
'http://example.com/images',
759 'transformVia404' =>
false,
761 'name' =>
'local-backend',
762 'wikiId' => wfWikiId(),
763 'containerPaths' =>
array(
764 'local-public' => $this->uploadDir,
765 'local-thumb' => $this->uploadDir .
'/thumb',
766 'local-temp' => $this->uploadDir .
'/temp',
767 'local-deleted' => $this->uploadDir .
'/delete',
771 'wgEnableUploads' => self::getOptionValue(
'wgEnableUploads', $opts,
true ),
772 'wgStylePath' =>
'/skins',
773 'wgSitename' =>
'MediaWiki',
774 'wgLanguageCode' => $lang,
775 'wgDBprefix' => $this->db->getType() !=
'oracle' ?
'parsertest_' :
'pt_',
776 'wgRawHtml' => self::getOptionValue(
'wgRawHtml', $opts,
false ),
778 'wgContLang' =>
null,
779 'wgNamespacesWithSubpages' =>
array( 0 => isset( $opts[
'subpage'] ) ),
780 'wgMaxTocLevel' => $maxtoclevel,
781 'wgCapitalLinks' =>
true,
782 'wgNoFollowLinks' =>
true,
783 'wgNoFollowDomainExceptions' =>
array(),
784 'wgThumbnailScriptPath' =>
false,
785 'wgUseImageResize' =>
true,
786 'wgSVGConverter' =>
'null',
787 'wgSVGConverters' =>
array(
'null' =>
'echo "1">$output' ),
788 'wgLocaltimezone' =>
'UTC',
789 'wgAllowExternalImages' => self::getOptionValue(
'wgAllowExternalImages', $opts,
true ),
790 'wgThumbLimits' =>
array( self::getOptionValue(
'thumbsize', $opts, 180 ) ),
791 'wgUseTidy' =>
false,
792 'wgDefaultLanguageVariant' => $variant,
793 'wgVariantArticlePath' =>
false,
795 'createaccount' =>
true,
798 'createpage' =>
true,
799 'createtalk' =>
true,
802 'wgDefaultExternalStore' =>
array(),
803 'wgForeignFileRepos' =>
array(),
804 'wgLinkHolderBatchSize' => $linkHolderBatchSize,
805 'wgExperimentalHtmlIds' =>
false,
806 'wgExternalLinkTarget' =>
false,
807 'wgAlwaysUseTidy' =>
false,
809 'wgWellFormedXml' =>
true,
810 'wgAllowMicrodataAttributes' =>
true,
811 'wgAdaptiveMessageCache' =>
true,
812 'wgDisableLangConversion' =>
false,
813 'wgDisableTitleConversion' =>
false,
817 $configLines = explode(
"\n", $config );
819 foreach ( $configLines
as $line ) {
822 $settings[$var] = eval(
"return $value;" );
826 $this->savedGlobals =
array();
831 foreach ( $settings
as $var => $val ) {
832 if ( array_key_exists( $var,
$GLOBALS ) ) {
833 $this->savedGlobals[$var] =
$GLOBALS[$var];
843 $GLOBALS[
'wgLang'] = $context->getLanguage();
844 $GLOBALS[
'wgOut'] = $context->getOutput();
845 $GLOBALS[
'wgUser'] = $context->getUser();
848 $context->getUser()->setOption(
'thumbsize', 0 );
852 $wgHooks[
'ParserTestParser'][] =
'ParserTestParserHook::setup';
853 $wgHooks[
'ParserGetVariableValueTs'][] =
'ParserTest::getFakeTimestamp';
864 private function listTables() {
865 $tables =
array(
'user',
'user_properties',
'user_former_groups',
'page',
'page_restrictions',
866 'protected_titles',
'revision',
'text',
'pagelinks',
'imagelinks',
867 'categorylinks',
'templatelinks',
'externallinks',
'langlinks',
'iwlinks',
868 'site_stats',
'hitcounter',
'ipblocks',
'image',
'oldimage',
869 'recentchanges',
'watchlist',
'interwiki',
'logging',
870 'querycache',
'objectcache',
'job',
'l10n_cache',
'redirect',
'querycachetwo',
871 'archive',
'user_groups',
'page_props',
'category',
'msg_resource',
'msg_resource_links'
874 if ( in_array( $this->db->getType(),
array(
'mysql',
'sqlite',
'oracle' ) ) ) {
875 array_push(
$tables,
'searchindex' );
891 public function setupDatabase() {
894 if ( $this->databaseSetupDone ) {
899 $dbType = $this->db->getType();
901 if ( $wgDBprefix ===
'parsertest_' || ( $dbType ==
'oracle' && $wgDBprefix ===
'pt_' ) ) {
902 throw new MWException(
'setupDatabase should be called before setupGlobals' );
905 $this->databaseSetupDone =
true;
906 $this->oldTablePrefix = $wgDBprefix;
908 # SqlBagOStuff broke when using temporary tables on r40209 (bug 15892).
909 # It seems to have been fixed since (r55079?), but regressed at some point before r85701.
910 # This works around it for now...
913 # CREATE TEMPORARY TABLE breaks if there is more than one server
914 if (
wfGetLB()->getServerCount() != 1 ) {
915 $this->useTemporaryTables =
false;
918 $temporary = $this->useTemporaryTables || $dbType ==
'postgres';
919 $prefix = $dbType !=
'oracle' ?
'parsertest_' :
'pt_';
921 $this->dbClone =
new CloneDatabase( $this->db, $this->listTables(), $prefix );
922 $this->dbClone->useTemporaryTables( $temporary );
923 $this->dbClone->cloneTableStructure();
925 if ( $dbType ==
'oracle' ) {
926 $this->db->query(
'BEGIN FILL_WIKI_INFO; END;' );
927 # Insert 0 user to prevent FK violations
930 $this->db->insert(
'user',
array(
932 'user_name' =>
'Anonymous' ) );
935 # Update certain things in site_stats
936 $this->db->insert(
'site_stats',
937 array(
'ss_row_id' => 1,
'ss_images' => 2,
'ss_good_articles' => 1 ) );
939 # Reinitialise the LocalisationCache to match the database state
942 # Clear the message cache
947 $this->uploadDir = $this->setupUploadDir();
950 # note that the size/width/height/bits/etc of the file
951 # are actually set by inspecting the file itself; the arguments
952 # to recordUpload2 have no effect. That said, we try to make things
953 # match up so it is less confusing to readers of the code & tests.
954 $image->recordUpload2(
'',
'Upload of some lame file',
'Some lame file',
array(
960 'mime' =>
'image/jpeg',
961 'metadata' => serialize(
array() ),
964 ), $this->db->timestamp(
'20010115123500' ),
$user );
967 # again, note that size/width/height below are ignored; see above.
968 $image->recordUpload2(
'',
'Upload of some lame thumbnail',
'Some lame thumbnail',
array(
974 'mime' =>
'image/png',
975 'metadata' => serialize(
array() ),
978 ), $this->db->timestamp(
'20130225203040' ),
$user );
981 $image->recordUpload2(
'',
'Upload of some lame SVG',
'Some lame SVG',
array(
987 'mime' =>
'image/svg+xml',
988 'metadata' => serialize(
array() ),
991 ), $this->db->timestamp(
'20010115123500' ),
$user );
993 # This image will be blacklisted in [[MediaWiki:Bad image list]]
995 $image->recordUpload2(
'',
'zomgnotcensored',
'Borderline image',
array(
1001 'mime' =>
'image/jpeg',
1002 'metadata' => serialize(
array() ),
1004 'fileExists' =>
true
1005 ), $this->db->timestamp(
'20010115123500' ),
$user );
1008 public function teardownDatabase() {
1009 if ( !$this->databaseSetupDone ) {
1010 $this->teardownGlobals();
1013 $this->teardownUploadDir( $this->uploadDir );
1015 $this->dbClone->destroy();
1016 $this->databaseSetupDone =
false;
1018 if ( $this->useTemporaryTables ) {
1019 if ( $this->db->getType() ==
'sqlite' ) {
1020 # Under SQLite the searchindex table is virtual and need
1021 # to be explicitly destroyed. See bug 29912
1022 # See also MediaWikiTestCase::destroyDB()
1023 wfDebug( __METHOD__ .
" explicitly destroying sqlite virtual table parsertest_searchindex\n" );
1024 $this->db->query(
"DROP TABLE `parsertest_searchindex`" );
1026 # Don't need to do anything
1027 $this->teardownGlobals();
1031 $tables = $this->listTables();
1034 if ( $this->db->getType() ==
'oracle' ) {
1035 $this->db->query(
"DROP TABLE pt_$table DROP CONSTRAINTS" );
1037 $this->db->query(
"DROP TABLE `parsertest_$table`" );
1041 if ( $this->db->getType() ==
'oracle' ) {
1042 $this->db->query(
'BEGIN FILL_WIKI_INFO; END;' );
1045 $this->teardownGlobals();
1054 private function setupUploadDir() {
1057 if ( $this->keepUploads ) {
1060 if ( is_dir(
$dir ) ) {
1068 if ( file_exists(
$dir ) ) {
1069 wfDebug(
"Already exists!\n" );
1074 copy(
"$IP/skins/monobook/headbg.jpg",
"$dir/3/3a/Foobar.jpg" );
1076 copy(
"$IP/skins/monobook/wiki.png",
"$dir/e/ea/Thumb.png" );
1078 copy(
"$IP/skins/monobook/headbg.jpg",
"$dir/0/09/Bad.jpg" );
1080 file_put_contents(
"$dir/f/ff/Foobar.svg",
1081 '<?xml version="1.0" encoding="utf-8"?>' .
1082 '<svg xmlns="http://www.w3.org/2000/svg"' .
1083 ' version="1.1" width="240" height="180"/>' );
1091 private function teardownGlobals() {
1097 foreach ( $this->savedGlobals
as $var => $val ) {
1105 private function teardownUploadDir(
$dir ) {
1106 if ( $this->keepUploads ) {
1113 "$dir/3/3a/Foobar.jpg",
1114 "$dir/thumb/3/3a/Foobar.jpg/1000px-Foobar.jpg",
1115 "$dir/thumb/3/3a/Foobar.jpg/100px-Foobar.jpg",
1116 "$dir/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg",
1117 "$dir/thumb/3/3a/Foobar.jpg/1280px-Foobar.jpg",
1118 "$dir/thumb/3/3a/Foobar.jpg/137px-Foobar.jpg",
1119 "$dir/thumb/3/3a/Foobar.jpg/1500px-Foobar.jpg",
1120 "$dir/thumb/3/3a/Foobar.jpg/177px-Foobar.jpg",
1121 "$dir/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg",
1122 "$dir/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg",
1123 "$dir/thumb/3/3a/Foobar.jpg/206px-Foobar.jpg",
1124 "$dir/thumb/3/3a/Foobar.jpg/20px-Foobar.jpg",
1125 "$dir/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg",
1126 "$dir/thumb/3/3a/Foobar.jpg/265px-Foobar.jpg",
1127 "$dir/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg",
1128 "$dir/thumb/3/3a/Foobar.jpg/274px-Foobar.jpg",
1129 "$dir/thumb/3/3a/Foobar.jpg/300px-Foobar.jpg",
1130 "$dir/thumb/3/3a/Foobar.jpg/30px-Foobar.jpg",
1131 "$dir/thumb/3/3a/Foobar.jpg/330px-Foobar.jpg",
1132 "$dir/thumb/3/3a/Foobar.jpg/353px-Foobar.jpg",
1133 "$dir/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg",
1134 "$dir/thumb/3/3a/Foobar.jpg/400px-Foobar.jpg",
1135 "$dir/thumb/3/3a/Foobar.jpg/40px-Foobar.jpg",
1136 "$dir/thumb/3/3a/Foobar.jpg/440px-Foobar.jpg",
1137 "$dir/thumb/3/3a/Foobar.jpg/442px-Foobar.jpg",
1138 "$dir/thumb/3/3a/Foobar.jpg/450px-Foobar.jpg",
1139 "$dir/thumb/3/3a/Foobar.jpg/50px-Foobar.jpg",
1140 "$dir/thumb/3/3a/Foobar.jpg/600px-Foobar.jpg",
1141 "$dir/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg",
1142 "$dir/thumb/3/3a/Foobar.jpg/70px-Foobar.jpg",
1143 "$dir/thumb/3/3a/Foobar.jpg/75px-Foobar.jpg",
1144 "$dir/thumb/3/3a/Foobar.jpg/960px-Foobar.jpg",
1146 "$dir/e/ea/Thumb.png",
1148 "$dir/0/09/Bad.jpg",
1150 "$dir/f/ff/Foobar.svg",
1151 "$dir/thumb/f/ff/Foobar.svg/180px-Foobar.svg.png",
1152 "$dir/thumb/f/ff/Foobar.svg/2000px-Foobar.svg.png",
1153 "$dir/thumb/f/ff/Foobar.svg/270px-Foobar.svg.png",
1154 "$dir/thumb/f/ff/Foobar.svg/3000px-Foobar.svg.png",
1155 "$dir/thumb/f/ff/Foobar.svg/360px-Foobar.svg.png",
1156 "$dir/thumb/f/ff/Foobar.svg/4000px-Foobar.svg.png",
1157 "$dir/thumb/f/ff/Foobar.svg/langde-180px-Foobar.svg.png",
1158 "$dir/thumb/f/ff/Foobar.svg/langde-270px-Foobar.svg.png",
1159 "$dir/thumb/f/ff/Foobar.svg/langde-360px-Foobar.svg.png",
1161 "$dir/math/f/a/5/fa50b8b616463173474302ca3e63586b.png",
1169 "$dir/thumb/3/3a/Foobar.jpg",
1176 "$dir/thumb/f/ff/Foobar.svg",
1195 private static function deleteFiles(
$files ) {
1197 if ( file_exists(
$file ) ) {
1207 private static function deleteDirs(
$dirs ) {
1209 if ( is_dir(
$dir ) ) {
1218 protected function showTesting( $desc ) {
1219 print
"Running test $desc... ";
1231 if ( $this->showProgress ) {
1232 print $this->
term->color(
'1;32' ) .
'PASSED' . $this->
term->reset() .
"\n";
1248 if ( $this->showFailure ) {
1249 if ( !$this->showProgress ) {
1250 # In quiet mode we didn't show the 'Testing' message before the
1251 # test, in case it succeeded. Show it now:
1252 $this->showTesting( $testResult->description );
1255 print $this->
term->color(
'31' ) .
'FAILED!' . $this->
term->reset() .
"\n";
1257 if ( $this->showOutput ) {
1258 print
"--- Expected ---\n{$testResult->expected}\n";
1259 print
"--- Actual ---\n{$testResult->actual}\n";
1263 print $this->quickDiff( $testResult->expected, $testResult->actual );
1264 if ( !$this->wellFormed( $testResult->actual ) ) {
1265 print
"XML error: $this->mXmlError\n";
1283 protected function quickDiff( $input,
$output,
1284 $inFileTail =
'expected', $outFileTail =
'actual'
1286 # Windows, or at least the fc utility, is retarded
1288 $prefix =
wfTempDir() .
"{$slash}mwParser-" . mt_rand();
1290 $infile =
"$prefix-$inFileTail";
1291 $this->dumpToFile( $input, $infile );
1293 $outfile =
"$prefix-$outFileTail";
1294 $this->dumpToFile(
$output, $outfile );
1301 $shellCommand = (
wfIsWindows() && !$wgDiff3 ) ?
'fc' :
'diff -au';
1303 $diff =
wfShellExec(
"$shellCommand $shellInfile $shellOutfile" );
1308 return $this->colorDiff( $diff );
1317 private function dumpToFile( $data, $filename ) {
1318 $file = fopen( $filename,
"wt" );
1319 fwrite(
$file, $data .
"\n" );
1330 protected function colorDiff( $text ) {
1331 return preg_replace(
1332 array(
'/^(-.*)$/m',
'/^(\+.*)$/m' ),
1333 array( $this->
term->color( 34 ) .
'$1' . $this->
term->reset(),
1334 $this->
term->color( 31 ) .
'$1' . $this->
term->reset() ),
1343 public function showRunFile(
$path ) {
1344 print $this->
term->color( 1 ) .
1345 "Reading tests from \"$path\"..." .
1346 $this->
term->reset() .
1357 public static function addArticle(
$name, $text,
$line =
'unknown', $ignoreDuplicate =
'' ) {
1360 $oldCapitalLinks = $wgCapitalLinks;
1361 $wgCapitalLinks =
true;
1363 $text = self::chomp( $text );
1368 if ( is_null(
$title ) ) {
1369 throw new MWException(
"invalid title '$name' at line $line\n" );
1373 $page->loadPageData(
'fromdbmaster' );
1375 if ( $page->exists() ) {
1376 if ( $ignoreDuplicate ==
'ignoreduplicate' ) {
1379 throw new MWException(
"duplicate article '$name' at line $line\n" );
1385 $wgCapitalLinks = $oldCapitalLinks;
1396 public function requireHook(
$name ) {
1404 echo
" This test suite requires the '$name' hook extension, skipping.\n";
1419 public function requireFunctionHook(
$name ) {
1427 echo
" This test suite requires the '$name' function hook extension, skipping.\n";
1441 private function tidy( $text ) {
1451 private function wellFormed( $text ) {
1458 $parser = xml_parser_create(
"UTF-8" );
1460 # case folding violates XML standard, turn it off
1461 xml_parser_set_option(
$parser, XML_OPTION_CASE_FOLDING,
false );
1464 $err = xml_error_string( xml_get_error_code(
$parser ) );
1465 $position = xml_get_current_byte_index(
$parser );
1466 $fragment = $this->extractFragment(
$html, $position );
1467 $this->mXmlError =
"$err at byte $position:\n$fragment";
1478 private function extractFragment( $text, $position ) {
1479 $start = max( 0, $position - 10 );
1480 $before = $position - $start;
1482 $this->
term->color( 34 ) .
1483 substr( $text, $start, $before ) .
1484 $this->
term->color( 0 ) .
1485 $this->
term->color( 31 ) .
1486 $this->
term->color( 1 ) .
1487 substr( $text, $position, 1 ) .
1488 $this->
term->color( 0 ) .
1489 $this->
term->color( 34 ) .
1490 substr( $text, $position + 1, 9 ) .
1491 $this->
term->color( 0 ) .
1493 $display = str_replace(
"\n",
' ', $fragment );
1495 str_repeat(
' ', $before ) .
1496 $this->
term->color( 31 ) .
1498 $this->
term->color( 0 );
1500 return "$display\n$caret";
1503 static function getFakeTimestamp( &
$parser, &$ts ) {