31 use Wikimedia\ScopedCallback;
32 use Wikimedia\TestingAccessWrapper;
37 class ParserTestRunner {
45 private static $coreTestFiles = [
47 'extraParserTests.txt',
53 private $useTemporaryTables =
true;
58 private $setupDone = [
59 'staticSetup' =>
false,
60 'perTestSetup' =>
false,
61 'setupDatabase' =>
false,
62 'setDatabase' =>
false,
63 'setupUploads' =>
false,
81 private $tidyDriver =
null;
93 private $uploadDir =
null;
99 private $fileBackendName;
112 private $normalizationFunctions = [];
118 private $runDisabled;
124 private $disableSaveParse;
130 private $keepUploads;
137 $this->recorder = $recorder;
141 if ( in_array( $func, [
'removeTbody',
'trimWhitespace' ] ) ) {
142 $this->normalizationFunctions[] = $func;
145 "Warning: unknown normalization option \"$func\"\n" );
157 $this->keepUploads = !empty(
$options[
'keep-uploads'] );
159 $this->fileBackendName =
$options[
'file-backend'] ??
false;
161 $this->runDisabled = !empty(
$options[
'run-disabled'] );
163 $this->disableSaveParse = !empty(
$options[
'disable-save-parse'] );
165 if ( isset(
$options[
'upload-dir'] ) ) {
166 $this->uploadDir =
$options[
'upload-dir'];
175 public static function getParserTestFiles() {
179 $files = array_map(
function ( $item ) {
180 return __DIR__ .
"/$item";
181 }, self::$coreTestFiles );
188 foreach ( $registry->getAllThings()
as $info ) {
189 $dir = dirname( $info[
'path'] ) .
'/tests/parser';
190 if ( !file_exists( $dir ) ) {
194 $dirIterator =
new RecursiveIteratorIterator(
195 new RecursiveDirectoryIterator( $dir )
197 foreach ( $dirIterator
as $fileInfo ) {
199 if ( substr( $fileInfo->getFilename(), -4 ) ===
'.txt' ) {
200 $name = $info[
'name'] . $counter;
201 while ( isset( $files[
$name] ) ) {
202 $name = $info[
'name'] .
'_' . $counter++;
204 $files[
$name] = $fileInfo->getPathname();
209 return array_unique( $files );
212 public function getRecorder() {
213 return $this->recorder;
235 public function staticSetup( $nextTeardown =
null ) {
262 $teardown[] = $this->markSetupDone(
'staticSetup' );
265 $setup[
'wgSitename'] =
'MediaWiki';
266 $setup[
'wgServer'] =
'http://example.org';
267 $setup[
'wgServerName'] =
'example.org';
268 $setup[
'wgScriptPath'] =
'';
269 $setup[
'wgScript'] =
'/index.php';
270 $setup[
'wgResourceBasePath'] =
'';
271 $setup[
'wgStylePath'] =
'/skins';
272 $setup[
'wgExtensionAssetsPath'] =
'/extensions';
273 $setup[
'wgArticlePath'] =
'/wiki/$1';
274 $setup[
'wgActionPaths'] = [];
275 $setup[
'wgVariantArticlePath'] =
false;
276 $setup[
'wgUploadNavigationUrl'] =
false;
277 $setup[
'wgCapitalLinks'] =
true;
278 $setup[
'wgNoFollowLinks'] =
true;
279 $setup[
'wgNoFollowDomainExceptions'] = [
'no-nofollow.org' ];
280 $setup[
'wgExternalLinkTarget'] =
false;
281 $setup[
'wgLocaltimezone'] =
'UTC';
282 $setup[
'wgHtml5'] =
true;
283 $setup[
'wgDisableLangConversion'] =
false;
284 $setup[
'wgDisableTitleConversion'] =
false;
288 $setup[
'wgExtraInterlanguageLinkPrefixes'] = [
'mul' ];
293 $teardown[] =
function () {
298 $setup[
'wgLockManagers'] = [ [
299 'name' =>
'fsLockManager',
302 'name' =>
'nullLockManager',
305 $reset =
function () {
309 $teardown[] = $reset;
312 $setup[
'wgDefaultExternalStore'] =
false;
315 $setup[
'wgAdaptiveMessageCache'] =
true;
318 $setup[
'wgUseDatabaseMessages'] =
true;
319 $reset =
function () {
323 $teardown[] = $reset;
326 $setup[
'wgSVGConverter'] =
'null';
327 $setup[
'wgSVGConverters'] = [
'null' =>
'echo "1">$output' ];
331 $ts = $this->getFakeTimestamp();
334 $teardown[] =
function () {
338 $this->appendNamespaceSetup( $setup, $teardown );
341 $teardown[] = $this->setupInterwikis();
345 $setup[
'wgLocalInterwikis'] = [
'local',
'mi' ];
346 $reset =
function () {
347 $this->resetTitleServices();
350 $teardown[] = $reset;
353 MediaWikiServices::getInstance()->disableService(
'MediaHandlerFactory' );
354 MediaWikiServices::getInstance()->redefineService(
355 'MediaHandlerFactory',
357 $handlers =
$services->getMainConfig()->get(
'ParserTestMediaHandlers' );
361 $teardown[] =
function () {
362 MediaWikiServices::getInstance()->resetServiceForTesting(
'MediaHandlerFactory' );
373 $teardown[] =
function ()
use ( $savedCache ) {
378 $teardown[] = $this->executeSetupSnippets( $setup );
381 return $this->createTeardownObject( $teardown, $nextTeardown );
384 private function appendNamespaceSetup( &$setup, &$teardown ) {
387 $setup[
'wgExtraNamespaces'] = [
388 100 =>
'MemoryAlpha',
389 101 =>
'MemoryAlpha_talk'
393 $reset =
function () {
395 MediaWikiServices::getInstance()->getContentLanguage()->resetNamespaces();
398 $teardown[] = $reset;
405 protected function createRepoGroup() {
406 if ( $this->uploadDir ) {
407 if ( $this->fileBackendName ) {
408 throw new MWException(
'You cannot specify both use-filebackend and upload-dir' );
411 'name' =>
'local-backend',
413 'basePath' => $this->uploadDir,
416 } elseif ( $this->fileBackendName ) {
418 $name = $this->fileBackendName;
421 if ( $conf[
'name'] ===
$name ) {
425 if ( $useConfig ===
false ) {
426 throw new MWException(
"Unable to find file backend \"$name\"" );
428 $useConfig[
'name'] =
'local-backend';
429 unset( $useConfig[
'lockManager'] );
430 unset( $useConfig[
'fileJournal'] );
431 $class = $useConfig[
'class'];
432 $backend =
new $class( $useConfig );
434 # Replace with a mock. We do not care about generating real
435 # files on the filesystem, just need to expose the file
438 'name' =>
'local-backend',
447 'url' =>
'http://example.com/images',
449 'transformVia404' =>
false,
450 'backend' => $backend
469 protected function executeSetupSnippets( $setup ) {
472 if ( is_int(
$name ) ) {
479 return function ()
use ( $saved ) {
480 $this->executeSetupSnippets( $saved );
496 protected function createTeardownObject( $teardown, $nextTeardown =
null ) {
497 return new ScopedCallback(
function ()
use ( $teardown, $nextTeardown ) {
499 $teardown = array_reverse( $teardown );
501 $this->executeSetupSnippets( $teardown );
502 if ( $nextTeardown ) {
503 ScopedCallback::consume( $nextTeardown );
515 protected function markSetupDone( $funcName ) {
516 if ( $this->setupDone[$funcName] ) {
517 throw new MWException(
"$funcName is already done" );
519 $this->setupDone[$funcName] =
true;
520 return function ()
use ( $funcName ) {
521 $this->setupDone[$funcName] =
false;
531 protected function checkSetupDone( $funcName, $funcName2 =
null ) {
532 if ( !$this->setupDone[$funcName]
533 && ( $funcName ===
null || !$this->setupDone[$funcName2] )
535 throw new MWException(
"$funcName must be called before calling " .
546 public function isSetupDone( $funcName ) {
547 return $this->setupDone[$funcName] ??
false;
561 private function setupInterwikis() {
562 # Hack: insert a few Wikipedia in-project interwiki prefixes,
563 # for testing inter-language links
564 Hooks::register(
'InterwikiLoadPrefix',
function ( $prefix, &$iwData ) {
565 static $testInterwikis = [
567 'iw_url' =>
'http://doesnt.matter.org/$1',
572 'iw_url' =>
'http://en.wikipedia.org/wiki/$1',
577 'iw_url' =>
'http://www.usemod.com/cgi-bin/mb.pl?$1',
582 'iw_url' =>
'http://www.memory-alpha.org/en/index.php/$1',
587 'iw_url' =>
'http://zh.wikipedia.org/wiki/$1',
592 'iw_url' =>
'http://es.wikipedia.org/wiki/$1',
597 'iw_url' =>
'http://fr.wikipedia.org/wiki/$1',
602 'iw_url' =>
'http://ru.wikipedia.org/wiki/$1',
607 'iw_url' =>
'http://mi.wikipedia.org/wiki/$1',
612 'iw_url' =>
'http://wikisource.org/wiki/$1',
617 if ( array_key_exists( $prefix, $testInterwikis ) ) {
618 $iwData = $testInterwikis[$prefix];
626 MediaWikiServices::getInstance()->resetServiceForTesting(
'InterwikiLookup' );
631 MediaWikiServices::getInstance()->resetServiceForTesting(
'InterwikiLookup' );
639 private function resetTitleServices() {
640 $services = MediaWikiServices::getInstance();
641 $services->resetServiceForTesting(
'TitleFormatter' );
642 $services->resetServiceForTesting(
'TitleParser' );
643 $services->resetServiceForTesting(
'_MediaWikiTitleCodec' );
644 $services->resetServiceForTesting(
'LinkRenderer' );
645 $services->resetServiceForTesting(
'LinkRendererFactory' );
653 public static function chomp(
$s ) {
654 if ( substr(
$s, -1 ) ===
"\n" ) {
655 return substr(
$s, 0, -1 );
674 public function runTestsFromFiles( $filenames ) {
677 $teardownGuard = $this->staticSetup();
678 $teardownGuard = $this->setupDatabase( $teardownGuard );
679 $teardownGuard = $this->setupUploads( $teardownGuard );
681 $this->recorder->start();
685 foreach ( $filenames
as $filename ) {
687 'runDisabled' => $this->runDisabled,
688 'regex' => $this->regex ] );
691 if ( !$testFileInfo[
'tests'] ) {
695 $this->recorder->startSuite( $filename );
696 $ok = $this->runTests( $testFileInfo ) && $ok;
697 $this->recorder->endSuite( $filename );
700 $this->recorder->report();
701 }
catch ( DBError
$e ) {
702 $this->recorder->warning(
$e->getMessage() );
704 $this->recorder->end();
706 ScopedCallback::consume( $teardownGuard );
717 public function meetsRequirements( $requirements ) {
718 foreach ( $requirements
as $requirement ) {
719 switch ( $requirement[
'type'] ) {
721 $ok = $this->requireHook( $requirement[
'name'] );
724 $ok = $this->requireFunctionHook( $requirement[
'name'] );
726 case 'transparentHook':
727 $ok = $this->requireTransparentHook( $requirement[
'name'] );
744 public function runTests( $testFileInfo ) {
747 $this->checkSetupDone(
'staticSetup' );
750 if ( !$testFileInfo[
'tests'] ) {
755 if ( !$this->meetsRequirements( $testFileInfo[
'requirements'] ) ) {
756 foreach ( $testFileInfo[
'tests']
as $test ) {
757 $this->recorder->startTest( $test );
758 $this->recorder->skipped( $test,
'required extension not enabled' );
764 $this->addArticles( $testFileInfo[
'articles'] );
767 foreach ( $testFileInfo[
'tests']
as $test ) {
768 $this->recorder->startTest( $test );
770 $this->runTest( $test );
772 $ok = $ok &&
$result->isSuccess();
773 $this->recorder->record( $test,
$result );
786 function getParser( $preprocessor =
null ) {
813 public function runTest( $test ) {
814 wfDebug( __METHOD__ .
": running {$test['desc']}" );
815 $opts = $this->parseOptions( $test[
'options'] );
816 $teardownGuard = $this->perTestSetup( $test );
821 $options->setTimestamp( $this->getFakeTimestamp() );
823 if ( isset( $opts[
'tidy'] ) ) {
827 if ( isset( $opts[
'title'] ) ) {
828 $titleText = $opts[
'title'];
830 $titleText =
'Parser test';
833 if ( isset( $opts[
'maxincludesize'] ) ) {
834 $options->setMaxIncludeSize( $opts[
'maxincludesize'] );
836 if ( isset( $opts[
'maxtemplatedepth'] ) ) {
837 $options->setMaxTemplateDepth( $opts[
'maxtemplatedepth'] );
840 $local = isset( $opts[
'local'] );
841 $preprocessor = $opts[
'preprocessor'] ??
null;
842 $parser = $this->getParser( $preprocessor );
845 if ( isset( $opts[
'styletag'] ) ) {
849 $marker = Parser::MARKER_PREFIX .
'-style-' . md5(
$content ) . Parser::MARKER_SUFFIX;
851 return Html::inlineStyle( $marker,
'all', $attributes );
854 return Html::element(
'link', $attributes );
858 if ( isset( $opts[
'pst'] ) ) {
861 } elseif ( isset( $opts[
'msg'] ) ) {
863 } elseif ( isset( $opts[
'section'] ) ) {
866 } elseif ( isset( $opts[
'replace'] ) ) {
868 $replace = $opts[
'replace'][1];
870 } elseif ( isset( $opts[
'comment'] ) ) {
872 } elseif ( isset( $opts[
'preload'] ) ) {
877 'allowTOC' => !isset( $opts[
'notoc'] ),
878 'unwrap' => !isset( $opts[
'wrap'] ),
880 if ( isset( $opts[
'tidy'] ) ) {
881 $out = preg_replace(
'/\s+$/',
'',
$out );
884 if ( isset( $opts[
'showtitle'] ) ) {
885 if (
$output->getTitleText() ) {
889 $out =
"$title\n$out";
892 if ( isset( $opts[
'showindicators'] ) ) {
895 $indicators .=
"$id=$content\n";
900 if ( isset( $opts[
'ill'] ) ) {
901 $out = implode(
' ',
$output->getLanguageLinks() );
902 } elseif ( isset( $opts[
'cat'] ) ) {
908 $out .=
"cat=$name sort=$sortkey";
913 if ( isset(
$output ) && isset( $opts[
'showflags'] ) ) {
914 $actualFlags = array_keys( TestingAccessWrapper::newFromObject(
$output )->mFlags );
915 sort( $actualFlags );
916 $out .=
"\nflags=" . implode(
', ', $actualFlags );
919 ScopedCallback::consume( $teardownGuard );
921 $expected = $test[
'result'];
922 if (
count( $this->normalizationFunctions ) ) {
924 $test[
'expected'], $this->normalizationFunctions );
939 private static function getOptionValue( $key, $opts, $default ) {
940 $key = strtolower( $key );
941 return $opts[$key] ?? $default;
951 private function parseOptions( $instring ) {
960 (?<qstr> # Quoted string
962 (?:[^\\\\"] | \\\\.)*
968 [^"{}] | # Not a quoted string or object, or
969 (?&qstr) | # A quoted string, or
970 (?&json) # A json object (recursively)
976 (?&qstr) # Quoted val
984 (?&json) # JSON object
988 $regex =
'/' . $defs .
'\b
1004 $valueregex =
'/' . $defs .
'(?&value)/x';
1006 if ( preg_match_all( $regex, $instring,
$matches, PREG_SET_ORDER ) ) {
1008 $key = strtolower( $bits[
'k'] );
1009 if ( !isset( $bits[
'v'] ) ) {
1012 preg_match_all( $valueregex, $bits[
'v'], $vmatches );
1013 $opts[$key] = array_map( [ $this,
'cleanupOption' ], $vmatches[0] );
1014 if (
count( $opts[$key] ) == 1 ) {
1015 $opts[$key] = $opts[$key][0];
1023 private function cleanupOption(
$opt ) {
1024 if ( substr(
$opt, 0, 1 ) ==
'"' ) {
1025 return stripcslashes( substr(
$opt, 1, -1 ) );
1028 if ( substr(
$opt, 0, 2 ) ==
'[[' ) {
1029 return substr(
$opt, 2, -2 );
1032 if ( substr(
$opt, 0, 1 ) ==
'{' ) {
1047 public function perTestSetup( $test, $nextTeardown =
null ) {
1050 $this->checkSetupDone(
'setupDatabase',
'setDatabase' );
1051 $teardown[] = $this->markSetupDone(
'perTestSetup' );
1053 $opts = $this->parseOptions( $test[
'options'] );
1054 $config = $test[
'config'];
1058 self::getOptionValue(
'language', $opts,
'en' );
1060 self::getOptionValue(
'variant', $opts,
false );
1062 self::getOptionValue(
'wgMaxTocLevel', $opts, 999 );
1063 $linkHolderBatchSize =
1064 self::getOptionValue(
'wgLinkHolderBatchSize', $opts, 1000 );
1067 $skin = self::getOptionValue(
'skin', $opts,
'fallback' );
1070 'wgEnableUploads' => self::getOptionValue(
'wgEnableUploads', $opts,
true ),
1071 'wgLanguageCode' => $langCode,
1072 'wgRawHtml' => self::getOptionValue(
'wgRawHtml', $opts,
false ),
1073 'wgNamespacesWithSubpages' => array_fill_keys(
1076 'wgMaxTocLevel' => $maxtoclevel,
1077 'wgAllowExternalImages' => self::getOptionValue(
'wgAllowExternalImages', $opts,
true ),
1078 'wgThumbLimits' => [ self::getOptionValue(
'thumbsize', $opts, 180 ) ],
1079 'wgDefaultLanguageVariant' => $variant,
1080 'wgLinkHolderBatchSize' => $linkHolderBatchSize,
1083 'wgEnableMagicLinks' => self::getOptionValue(
'wgEnableMagicLinks', $opts, [] )
1084 + [
'ISBN' =>
true,
'PMID' =>
true,
'RFC' =>
true ],
1086 'wgFragmentMode' => [
'legacy' ],
1089 $nonIncludable = self::getOptionValue(
'wgNonincludableNamespaces', $opts,
false );
1090 if ( $nonIncludable !==
false ) {
1091 $setup[
'wgNonincludableNamespaces'] = [ $nonIncludable ];
1095 $configLines = explode(
"\n", $config );
1097 foreach ( $configLines
as $line ) {
1099 $setup[$var] = eval(
"return $value;" );
1104 Hooks::run(
'ParserTestGlobals', [ &$setup ] );
1107 if ( isset( $opts[
'tidy'] ) ) {
1109 if ( $this->tidyDriver ===
null ) {
1112 $tidy = $this->tidyDriver;
1117 # Suppress warnings about running tests without tidy
1118 Wikimedia\suppressWarnings();
1121 Wikimedia\restoreWarnings();
1124 $teardown[] =
function () {
1130 $lang->resetNamespaces();
1131 $setup[
'wgContLang'] =
$lang;
1132 $setup[] =
function ()
use (
$lang ) {
1133 MediaWikiServices::getInstance()->disableService(
'ContentLanguage' );
1134 MediaWikiServices::getInstance()->redefineService(
1141 $teardown[] =
function () {
1142 MediaWikiServices::getInstance()->resetServiceForTesting(
'ContentLanguage' );
1144 $reset =
function () {
1145 MediaWikiServices::getInstance()->resetServiceForTesting(
'MagicWordFactory' );
1146 $this->resetTitleServices();
1149 $teardown[] = $reset;
1153 $user->setOption(
'language', $langCode );
1154 $setup[
'wgLang'] =
$lang;
1157 $user->setOption(
'thumbsize', 0 );
1159 $setup[
'wgUser'] =
$user;
1167 $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
1170 $setup[
'wgOut'] =
$context->getOutput();
1171 $teardown[] =
function ()
use (
$context, $oldSkin ) {
1173 $wrapper = TestingAccessWrapper::newFromObject(
1174 $context->getLanguage()->getConverter()
1176 $wrapper->reloadTables();
1184 $teardown[] = $this->executeSetupSnippets( $setup );
1186 return $this->createTeardownObject( $teardown, $nextTeardown );
1194 private function listTables() {
1197 $tables = [
'user',
'user_properties',
'user_former_groups',
'page',
'page_restrictions',
1198 'protected_titles',
'revision',
'ip_changes',
'text',
'pagelinks',
'imagelinks',
1199 'categorylinks',
'templatelinks',
'externallinks',
'langlinks',
'iwlinks',
1200 'site_stats',
'ipblocks',
'image',
'oldimage',
1201 'recentchanges',
'watchlist',
'interwiki',
'logging',
'log_search',
1202 'querycache',
'objectcache',
'job',
'l10n_cache',
'redirect',
'querycachetwo',
1203 'archive',
'user_groups',
'page_props',
'category',
1204 'slots',
'content',
'slot_roles',
'content_models',
1205 'comment',
'revision_comment_temp',
1211 $tables[] =
'revision_actor_temp';
1214 if ( in_array( $this->db->getType(), [
'mysql',
'sqlite',
'oracle' ] ) ) {
1215 array_push(
$tables,
'searchindex' );
1226 public function setDatabase(
IDatabase $db ) {
1228 $this->setupDone[
'setDatabase'] =
true;
1248 public function setupDatabase( $nextTeardown =
null ) {
1252 $dbType = $this->db->getType();
1254 if ( $dbType ==
'oracle' ) {
1259 if ( in_array(
$wgDBprefix, $suspiciousPrefixes ) ) {
1260 throw new MWException(
"\$wgDBprefix=$wgDBprefix suggests DB setup is already done" );
1265 $teardown[] = $this->markSetupDone(
'setupDatabase' );
1267 # CREATE TEMPORARY TABLE breaks if there is more than one server
1268 if ( MediaWikiServices::getInstance()->getDBLoadBalancer()->getServerCount() != 1 ) {
1269 $this->useTemporaryTables =
false;
1272 $temporary = $this->useTemporaryTables || $dbType ==
'postgres';
1273 $prefix = $dbType !=
'oracle' ?
'parsertest_' :
'pt_';
1275 $this->dbClone =
new CloneDatabase( $this->db, $this->listTables(), $prefix );
1276 $this->dbClone->useTemporaryTables( $temporary );
1277 $this->dbClone->cloneTableStructure();
1280 if ( $dbType ==
'oracle' ) {
1281 $this->db->query(
'BEGIN FILL_WIKI_INFO; END;' );
1282 # Insert 0 user to prevent FK violations
1285 $this->db->insert(
'user', [
1287 'user_name' =>
'Anonymous' ] );
1290 $teardown[] =
function () {
1291 $this->teardownDatabase();
1295 $reset =
function () {
1296 MediaWikiServices::getInstance()->getLinkCache()->clear();
1302 $teardown[] = $reset;
1303 return $this->createTeardownObject( $teardown, $nextTeardown );
1314 public function setupUploads( $nextTeardown =
null ) {
1317 $this->checkSetupDone(
'setupDatabase',
'setDatabase' );
1318 $teardown[] = $this->markSetupDone(
'setupUploads' );
1322 $teardown[] = $this->setupUploadBackend();
1330 # note that the size/width/height/bits/etc of the file
1331 # are actually set by inspecting the file itself; the arguments
1332 # to recordUpload2 have no effect. That said, we try to make things
1333 # match up so it is less confusing to readers of the code & tests.
1334 $image->recordUpload2(
'',
'Upload of some lame file',
'Some lame file', [
1340 'mime' =>
'image/jpeg',
1342 'sha1' =>
Wikimedia\base_convert(
'1', 16, 36, 31 ),
1343 'fileExists' =>
true
1344 ], $this->db->timestamp(
'20010115123500' ),
$user );
1347 # again, note that size/width/height below are ignored; see above.
1348 $image->recordUpload2(
'',
'Upload of some lame thumbnail',
'Some lame thumbnail', [
1354 'mime' =>
'image/png',
1356 'sha1' =>
Wikimedia\base_convert(
'2', 16, 36, 31 ),
1357 'fileExists' =>
true
1358 ], $this->db->timestamp(
'20130225203040' ),
$user );
1361 $image->recordUpload2(
'',
'Upload of some lame SVG',
'Some lame SVG', [
1367 'mime' =>
'image/svg+xml',
1372 'originalWidth' =>
'100%',
1373 'originalHeight' =>
'100%',
1379 'sha1' =>
Wikimedia\base_convert(
'', 16, 36, 31 ),
1380 'fileExists' =>
true
1381 ], $this->db->timestamp(
'20010115123500' ),
$user );
1383 # This image will be blacklisted in [[MediaWiki:Bad image list]]
1385 $image->recordUpload2(
'',
'zomgnotcensored',
'Borderline image', [
1391 'mime' =>
'image/jpeg',
1393 'sha1' =>
Wikimedia\base_convert(
'3', 16, 36, 31 ),
1394 'fileExists' =>
true
1395 ], $this->db->timestamp(
'20010115123500' ),
$user );
1398 $image->recordUpload2(
'',
'A pretty movie',
'Will it play', [
1404 'mime' =>
'application/ogg',
1406 'sha1' =>
Wikimedia\base_convert(
'', 16, 36, 31 ),
1407 'fileExists' =>
true
1408 ], $this->db->timestamp(
'20010115123500' ),
$user );
1411 $image->recordUpload2(
'',
'An awesome hitsong',
'Will it play', [
1417 'mime' =>
'application/ogg',
1419 'sha1' =>
Wikimedia\base_convert(
'', 16, 36, 31 ),
1420 'fileExists' =>
true
1421 ], $this->db->timestamp(
'20010115123500' ),
$user );
1425 $image->recordUpload2(
'',
'Upload a DjVu',
'A DjVu', [
1431 'mime' =>
'image/vnd.djvu',
1432 'metadata' =>
'<?xml version="1.0" ?>
1433 <!DOCTYPE DjVuXML PUBLIC "-//W3C//DTD DjVuXML 1.1//EN" "pubtext/DjVuXML-s.dtd">
1436 <BODY><OBJECT height="3508" width="2480">
1437 <PARAM name="DPI" value="300" />
1438 <PARAM name="GAMMA" value="2.2" />
1440 <OBJECT height="3508" width="2480">
1441 <PARAM name="DPI" value="300" />
1442 <PARAM name="GAMMA" value="2.2" />
1444 <OBJECT height="3508" width="2480">
1445 <PARAM name="DPI" value="300" />
1446 <PARAM name="GAMMA" value="2.2" />
1448 <OBJECT height="3508" width="2480">
1449 <PARAM name="DPI" value="300" />
1450 <PARAM name="GAMMA" value="2.2" />
1452 <OBJECT height="3508" width="2480">
1453 <PARAM name="DPI" value="300" />
1454 <PARAM name="GAMMA" value="2.2" />
1458 'sha1' =>
Wikimedia\base_convert(
'', 16, 36, 31 ),
1459 'fileExists' =>
true
1460 ], $this->db->timestamp(
'20010115123600' ),
$user );
1462 return $this->createTeardownObject( $teardown, $nextTeardown );
1471 private function teardownDatabase() {
1472 $this->checkSetupDone(
'setupDatabase' );
1474 $this->dbClone->destroy();
1476 if ( $this->useTemporaryTables ) {
1477 if ( $this->db->getType() ==
'sqlite' ) {
1478 # Under SQLite the searchindex table is virtual and need
1479 # to be explicitly destroyed. See T31912
1480 # See also MediaWikiTestCase::destroyDB()
1481 wfDebug( __METHOD__ .
" explicitly destroying sqlite virtual table parsertest_searchindex\n" );
1482 $this->db->query(
"DROP TABLE `parsertest_searchindex`" );
1484 # Don't need to do anything
1488 $tables = $this->listTables();
1491 if ( $this->db->getType() ==
'oracle' ) {
1492 $this->db->query(
"DROP TABLE pt_$table DROP CONSTRAINTS" );
1494 $this->db->query(
"DROP TABLE `parsertest_$table`" );
1498 if ( $this->db->getType() ==
'oracle' ) {
1499 $this->db->query(
'BEGIN FILL_WIKI_INFO; END;' );
1508 private function setupUploadBackend() {
1512 $base = $repo->getZonePath(
'public' );
1513 $backend = $repo->getBackend();
1514 $backend->prepare( [
'dir' =>
"$base/3/3a" ] );
1516 'src' =>
"$IP/tests/phpunit/data/parser/headbg.jpg",
1517 'dst' =>
"$base/3/3a/Foobar.jpg"
1519 $backend->prepare( [
'dir' =>
"$base/e/ea" ] );
1521 'src' =>
"$IP/tests/phpunit/data/parser/wiki.png",
1522 'dst' =>
"$base/e/ea/Thumb.png"
1524 $backend->prepare( [
'dir' =>
"$base/0/09" ] );
1526 'src' =>
"$IP/tests/phpunit/data/parser/headbg.jpg",
1527 'dst' =>
"$base/0/09/Bad.jpg"
1529 $backend->prepare( [
'dir' =>
"$base/5/5f" ] );
1531 'src' =>
"$IP/tests/phpunit/data/parser/LoremIpsum.djvu",
1532 'dst' =>
"$base/5/5f/LoremIpsum.djvu"
1536 $data =
'<?xml version="1.0" encoding="utf-8"?>' .
1537 '<svg xmlns="http://www.w3.org/2000/svg"' .
1538 ' version="1.1" width="240" height="180"/>';
1540 $backend->prepare( [
'dir' =>
"$base/f/ff" ] );
1541 $backend->quickCreate( [
1542 'content' =>
$data,
'dst' =>
"$base/f/ff/Foobar.svg"
1545 return function ()
use ( $backend ) {
1550 $this->teardownUploadBackend();
1557 private function teardownUploadBackend() {
1558 if ( $this->keepUploads ) {
1563 $public = $repo->getZonePath(
'public' );
1567 "$public/3/3a/Foobar.jpg",
1568 "$public/e/ea/Thumb.png",
1569 "$public/0/09/Bad.jpg",
1570 "$public/5/5f/LoremIpsum.djvu",
1571 "$public/f/ff/Foobar.svg",
1572 "$public/0/00/Video.ogv",
1573 "$public/4/41/Audio.oga",
1582 private function deleteFiles( $files ) {
1586 $backend->delete( [
'src' =>
$file ], [
'force' => 1 ] );
1593 if ( !$backend->clean( [
'dir' => $tmp ] )->isOK() ) {
1606 public function addArticles( $articles ) {
1612 if ( MediaWikiServices::getInstance()->getContentLanguage()->getCode() !==
'en' ) {
1613 $setup[
'wgLanguageCode'] =
'en';
1615 $setup[
'wgContLang'] =
$lang;
1616 $setup[] =
function ()
use (
$lang ) {
1617 $services = MediaWikiServices::getInstance();
1618 $services->disableService(
'ContentLanguage' );
1623 $teardown[] =
function () {
1624 MediaWikiServices::getInstance()->resetServiceForTesting(
'ContentLanguage' );
1629 $this->appendNamespaceSetup( $setup, $teardown );
1632 $setup[
'wgCapitalLinks'] =
true;
1634 $teardown[] = $this->executeSetupSnippets( $setup );
1636 foreach ( $articles
as $info ) {
1637 $this->addArticle( $info[
'name'], $info[
'text'], $info[
'file'], $info[
'line'] );
1644 $this->executeSetupSnippets( $teardown );
1657 $text = self::chomp( $text );
1661 wfDebug( __METHOD__ .
": adding $name" );
1663 if ( is_null(
$title ) ) {
1664 throw new MWException(
"invalid title '$name' at $file:$line\n" );
1670 $page->loadPageData(
'fromdbmaster' );
1672 if ( $page->exists() ) {
1676 if ( $newContent->equals(
$content ) ) {
1680 "duplicate article '$name' with different content at $file:$line\n"
1687 if ( $this->disableSaveParse ) {
1694 $status = $page->doEditContent(
1721 public function requireHook(
$name ) {
1728 $this->recorder->warning(
" This test suite requires the '$name' hook " .
1729 "extension, skipping." );
1740 public function requireFunctionHook(
$name ) {
1748 $this->recorder->warning(
" This test suite requires the '$name' function " .
1749 "hook extension, skipping." );
1760 public function requireTransparentHook(
$name ) {
1768 $this->recorder->warning(
" This test suite requires the '$name' transparent " .
1769 "hook extension, skipping.\n" );
1781 private function getFakeTimestamp() {