MediaWiki REL1_30
MediaWikiTestCase.php
Go to the documentation of this file.
1<?php
2
7use Psr\Log\LoggerInterface;
10use Wikimedia\TestingAccessWrapper;
11
15abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
16
24 private static $serviceLocator = null;
25
38 private $called = [];
39
44 public static $users;
45
52 protected $db;
53
58 protected $tablesUsed = []; // tables with data
59
60 private static $useTemporaryTables = true;
61 private static $reuseDB = false;
62 private static $dbSetup = false;
63 private static $oldTablePrefix = '';
64
71
78 private $tmpFiles = [];
79
86 private $mwGlobals = [];
87
93 private $mwGlobalsToUnset = [];
94
99 private $loggers = [];
100
104 const DB_PREFIX = 'unittest_';
105 const ORA_DB_PREFIX = 'ut_';
106
111 protected $supportedDBs = [
112 'mysql',
113 'sqlite',
114 'postgres',
115 'oracle'
116 ];
117
118 public function __construct( $name = null, array $data = [], $dataName = '' ) {
119 parent::__construct( $name, $data, $dataName );
120
121 $this->backupGlobals = false;
122 $this->backupStaticAttributes = false;
123 }
124
125 public function __destruct() {
126 // Complain if self::setUp() was called, but not self::tearDown()
127 // $this->called['setUp'] will be checked by self::testMediaWikiTestCaseParentSetupCalled()
128 if ( isset( $this->called['setUp'] ) && !isset( $this->called['tearDown'] ) ) {
129 throw new MWException( static::class . "::tearDown() must call parent::tearDown()" );
130 }
131 }
132
133 public static function setUpBeforeClass() {
134 parent::setUpBeforeClass();
135
136 // Get the service locator, and reset services if it's not done already
137 self::$serviceLocator = self::prepareServices( new GlobalVarConfig() );
138 }
139
148 public static function getTestUser( $groups = [] ) {
150 }
151
160 public static function getMutableTestUser( $groups = [] ) {
161 return TestUserRegistry::getMutableTestUser( __CLASS__, $groups );
162 }
163
172 public static function getTestSysop() {
173 return self::getTestUser( [ 'sysop', 'bureaucrat' ] );
174 }
175
195 public static function prepareServices( Config $bootstrapConfig ) {
196 static $services = null;
197
198 if ( !$services ) {
199 $services = self::resetGlobalServices( $bootstrapConfig );
200 }
201 return $services;
202 }
203
218 protected static function resetGlobalServices( Config $bootstrapConfig = null ) {
219 $oldServices = MediaWikiServices::getInstance();
220 $oldConfigFactory = $oldServices->getConfigFactory();
221 $oldLoadBalancerFactory = $oldServices->getDBLoadBalancerFactory();
222
223 $testConfig = self::makeTestConfig( $bootstrapConfig );
224
225 MediaWikiServices::resetGlobalInstance( $testConfig );
226
227 $serviceLocator = MediaWikiServices::getInstance();
228 self::installTestServices(
229 $oldConfigFactory,
230 $oldLoadBalancerFactory,
232 );
233 return $serviceLocator;
234 }
235
245 private static function makeTestConfig(
246 Config $baseConfig = null,
247 Config $customOverrides = null
248 ) {
249 $defaultOverrides = new HashConfig();
250
251 if ( !$baseConfig ) {
252 $baseConfig = MediaWikiServices::getInstance()->getBootstrapConfig();
253 }
254
255 /* Some functions require some kind of caching, and will end up using the db,
256 * which we can't allow, as that would open a new connection for mysql.
257 * Replace with a HashBag. They would not be going to persist anyway.
258 */
259 $hashCache = [ 'class' => 'HashBagOStuff', 'reportDupes' => false ];
260 $objectCaches = [
261 CACHE_DB => $hashCache,
262 CACHE_ACCEL => $hashCache,
263 CACHE_MEMCACHED => $hashCache,
264 'apc' => $hashCache,
265 'apcu' => $hashCache,
266 'xcache' => $hashCache,
267 'wincache' => $hashCache,
268 ] + $baseConfig->get( 'ObjectCaches' );
269
270 $defaultOverrides->set( 'ObjectCaches', $objectCaches );
271 $defaultOverrides->set( 'MainCacheType', CACHE_NONE );
272 $defaultOverrides->set( 'JobTypeConf', [ 'default' => [ 'class' => 'JobQueueMemory' ] ] );
273
274 // Use a fast hash algorithm to hash passwords.
275 $defaultOverrides->set( 'PasswordDefault', 'A' );
276
277 $testConfig = $customOverrides
278 ? new MultiConfig( [ $customOverrides, $defaultOverrides, $baseConfig ] )
279 : new MultiConfig( [ $defaultOverrides, $baseConfig ] );
280
281 return $testConfig;
282 }
283
291 private static function installTestServices(
292 ConfigFactory $oldConfigFactory,
293 LBFactory $oldLoadBalancerFactory,
294 MediaWikiServices $newServices
295 ) {
296 // Use bootstrap config for all configuration.
297 // This allows config overrides via global variables to take effect.
298 $bootstrapConfig = $newServices->getBootstrapConfig();
299 $newServices->resetServiceForTesting( 'ConfigFactory' );
300 $newServices->redefineService(
301 'ConfigFactory',
302 self::makeTestConfigFactoryInstantiator(
303 $oldConfigFactory,
304 [ 'main' => $bootstrapConfig ]
305 )
306 );
307 $newServices->resetServiceForTesting( 'DBLoadBalancerFactory' );
308 $newServices->redefineService(
309 'DBLoadBalancerFactory',
310 function ( MediaWikiServices $services ) use ( $oldLoadBalancerFactory ) {
311 return $oldLoadBalancerFactory;
312 }
313 );
314 }
315
322 private static function makeTestConfigFactoryInstantiator(
323 ConfigFactory $oldFactory,
324 array $configurations
325 ) {
326 return function ( MediaWikiServices $services ) use ( $oldFactory, $configurations ) {
327 $factory = new ConfigFactory();
328
329 // clone configurations from $oldFactory that are not overwritten by $configurations
330 $namesToClone = array_diff(
331 $oldFactory->getConfigNames(),
332 array_keys( $configurations )
333 );
334
335 foreach ( $namesToClone as $name ) {
336 $factory->register( $name, $oldFactory->makeConfig( $name ) );
337 }
338
339 foreach ( $configurations as $name => $config ) {
340 $factory->register( $name, $config );
341 }
342
343 return $factory;
344 };
345 }
346
358 private function doLightweightServiceReset() {
360
362 ObjectCache::clear();
363 $services = MediaWikiServices::getInstance();
364 $services->resetServiceForTesting( 'MainObjectStash' );
365 $services->resetServiceForTesting( 'LocalServerObjectCache' );
366 $services->getMainWANObjectCache()->clearProcessCache();
368
369 // TODO: move global state into MediaWikiServices
371 if ( session_id() !== '' ) {
372 session_write_close();
373 session_id( '' );
374 }
375
376 $wgRequest = new FauxRequest();
377 MediaWiki\Session\SessionManager::resetCache();
378 }
379
380 public function run( PHPUnit_Framework_TestResult $result = null ) {
381 // Reset all caches between tests.
383
384 $needsResetDB = false;
385
386 if ( !self::$dbSetup || $this->needsDB() ) {
387 // set up a DB connection for this test to use
388
389 self::$useTemporaryTables = !$this->getCliArg( 'use-normal-tables' );
390 self::$reuseDB = $this->getCliArg( 'reuse-db' );
391
392 $this->db = wfGetDB( DB_MASTER );
393
394 $this->checkDbIsSupported();
395
396 if ( !self::$dbSetup ) {
397 $this->setupAllTestDBs();
398 $this->addCoreDBData();
399
400 if ( ( $this->db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
401 $this->resetDB( $this->db, $this->tablesUsed );
402 }
403 }
404
405 // TODO: the DB setup should be done in setUpBeforeClass(), so the test DB
406 // is available in subclass's setUpBeforeClass() and setUp() methods.
407 // This would also remove the need for the HACK that is oncePerClass().
408 if ( $this->oncePerClass() ) {
409 $this->addDBDataOnce();
410 }
411
412 $this->addDBData();
413 $needsResetDB = true;
414 }
415
416 parent::run( $result );
417
418 if ( $needsResetDB ) {
419 $this->resetDB( $this->db, $this->tablesUsed );
420 }
421 }
422
426 private function oncePerClass() {
427 // Remember current test class in the database connection,
428 // so we know when we need to run addData.
429
430 $class = static::class;
431
432 $first = !isset( $this->db->_hasDataForTestClass )
433 || $this->db->_hasDataForTestClass !== $class;
434
435 $this->db->_hasDataForTestClass = $class;
436 return $first;
437 }
438
444 public function usesTemporaryTables() {
445 return self::$useTemporaryTables;
446 }
447
457 protected function getNewTempFile() {
458 $fileName = tempnam( wfTempDir(), 'MW_PHPUnit_' . static::class . '_' );
459 $this->tmpFiles[] = $fileName;
460
461 return $fileName;
462 }
463
474 protected function getNewTempDirectory() {
475 // Starting of with a temporary /file/.
476 $fileName = $this->getNewTempFile();
477
478 // Converting the temporary /file/ to a /directory/
479 // The following is not atomic, but at least we now have a single place,
480 // where temporary directory creation is bundled and can be improved
481 unlink( $fileName );
482 $this->assertTrue( wfMkdirParents( $fileName ) );
483
484 return $fileName;
485 }
486
487 protected function setUp() {
488 parent::setUp();
489 $this->called['setUp'] = true;
490
491 $this->phpErrorLevel = intval( ini_get( 'error_reporting' ) );
492
493 // Cleaning up temporary files
494 foreach ( $this->tmpFiles as $fileName ) {
495 if ( is_file( $fileName ) || ( is_link( $fileName ) ) ) {
496 unlink( $fileName );
497 } elseif ( is_dir( $fileName ) ) {
498 wfRecursiveRemoveDir( $fileName );
499 }
500 }
501
502 if ( $this->needsDB() && $this->db ) {
503 // Clean up open transactions
504 while ( $this->db->trxLevel() > 0 ) {
505 $this->db->rollback( __METHOD__, 'flush' );
506 }
507 // Check for unsafe queries
508 if ( $this->db->getType() === 'mysql' ) {
509 $this->db->query( "SET sql_mode = 'STRICT_ALL_TABLES'" );
510 }
511 }
512
513 DeferredUpdates::clearPendingUpdates();
514 ObjectCache::getMainWANInstance()->clearProcessCache();
515
516 // XXX: reset maintenance triggers
517 // Hook into period lag checks which often happen in long-running scripts
518 $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
520
521 ob_start( 'MediaWikiTestCase::wfResetOutputBuffersBarrier' );
522 }
523
524 protected function addTmpFiles( $files ) {
525 $this->tmpFiles = array_merge( $this->tmpFiles, (array)$files );
526 }
527
528 protected function tearDown() {
530
531 $status = ob_get_status();
532 if ( isset( $status['name'] ) &&
533 $status['name'] === 'MediaWikiTestCase::wfResetOutputBuffersBarrier'
534 ) {
535 ob_end_flush();
536 }
537
538 $this->called['tearDown'] = true;
539 // Cleaning up temporary files
540 foreach ( $this->tmpFiles as $fileName ) {
541 if ( is_file( $fileName ) || ( is_link( $fileName ) ) ) {
542 unlink( $fileName );
543 } elseif ( is_dir( $fileName ) ) {
544 wfRecursiveRemoveDir( $fileName );
545 }
546 }
547
548 if ( $this->needsDB() && $this->db ) {
549 // Clean up open transactions
550 while ( $this->db->trxLevel() > 0 ) {
551 $this->db->rollback( __METHOD__, 'flush' );
552 }
553 if ( $this->db->getType() === 'mysql' ) {
554 $this->db->query( "SET sql_mode = " . $this->db->addQuotes( $wgSQLMode ) );
555 }
556 }
557
558 // Restore mw globals
559 foreach ( $this->mwGlobals as $key => $value ) {
560 $GLOBALS[$key] = $value;
561 }
562 foreach ( $this->mwGlobalsToUnset as $value ) {
563 unset( $GLOBALS[$value] );
564 }
565 $this->mwGlobals = [];
566 $this->mwGlobalsToUnset = [];
567 $this->restoreLoggers();
568
569 if ( self::$serviceLocator && MediaWikiServices::getInstance() !== self::$serviceLocator ) {
570 MediaWikiServices::forceGlobalInstance( self::$serviceLocator );
571 }
572
573 // TODO: move global state into MediaWikiServices
575 if ( session_id() !== '' ) {
576 session_write_close();
577 session_id( '' );
578 }
579 $wgRequest = new FauxRequest();
580 MediaWiki\Session\SessionManager::resetCache();
581 MediaWiki\Auth\AuthManager::resetCache();
582
583 $phpErrorLevel = intval( ini_get( 'error_reporting' ) );
584
585 if ( $phpErrorLevel !== $this->phpErrorLevel ) {
586 ini_set( 'error_reporting', $this->phpErrorLevel );
587
588 $oldHex = strtoupper( dechex( $this->phpErrorLevel ) );
589 $newHex = strtoupper( dechex( $phpErrorLevel ) );
590 $message = "PHP error_reporting setting was left dirty: "
591 . "was 0x$oldHex before test, 0x$newHex after test!";
592
593 $this->fail( $message );
594 }
595
596 parent::tearDown();
597 }
598
608 $this->assertArrayHasKey( 'setUp', $this->called,
609 static::class . '::setUp() must call parent::setUp()'
610 );
611 }
612
622 protected function setService( $name, $object ) {
623 // If we did not yet override the service locator, so so now.
624 if ( MediaWikiServices::getInstance() === self::$serviceLocator ) {
625 $this->overrideMwServices();
626 }
627
628 MediaWikiServices::getInstance()->disableService( $name );
629 MediaWikiServices::getInstance()->redefineService(
630 $name,
631 function () use ( $object ) {
632 return $object;
633 }
634 );
635 }
636
672 protected function setMwGlobals( $pairs, $value = null ) {
673 if ( is_string( $pairs ) ) {
674 $pairs = [ $pairs => $value ];
675 }
676
677 $this->stashMwGlobals( array_keys( $pairs ) );
678
679 foreach ( $pairs as $key => $value ) {
680 $GLOBALS[$key] = $value;
681 }
682 }
683
692 private static function canShallowCopy( $value ) {
693 if ( is_scalar( $value ) || $value === null ) {
694 return true;
695 }
696 if ( is_array( $value ) ) {
697 foreach ( $value as $subValue ) {
698 if ( !is_scalar( $subValue ) && $subValue !== null ) {
699 return false;
700 }
701 }
702 return true;
703 }
704 return false;
705 }
706
724 protected function stashMwGlobals( $globalKeys ) {
725 if ( is_string( $globalKeys ) ) {
726 $globalKeys = [ $globalKeys ];
727 }
728
729 foreach ( $globalKeys as $globalKey ) {
730 // NOTE: make sure we only save the global once or a second call to
731 // setMwGlobals() on the same global would override the original
732 // value.
733 if (
734 !array_key_exists( $globalKey, $this->mwGlobals ) &&
735 !array_key_exists( $globalKey, $this->mwGlobalsToUnset )
736 ) {
737 if ( !array_key_exists( $globalKey, $GLOBALS ) ) {
738 $this->mwGlobalsToUnset[$globalKey] = $globalKey;
739 continue;
740 }
741 // NOTE: we serialize then unserialize the value in case it is an object
742 // this stops any objects being passed by reference. We could use clone
743 // and if is_object but this does account for objects within objects!
744 if ( self::canShallowCopy( $GLOBALS[$globalKey] ) ) {
745 $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
746 } elseif (
747 // Many MediaWiki types are safe to clone. These are the
748 // ones that are most commonly stashed.
749 $GLOBALS[$globalKey] instanceof Language ||
750 $GLOBALS[$globalKey] instanceof User ||
751 $GLOBALS[$globalKey] instanceof FauxRequest
752 ) {
753 $this->mwGlobals[$globalKey] = clone $GLOBALS[$globalKey];
754 } elseif ( $this->containsClosure( $GLOBALS[$globalKey] ) ) {
755 // Serializing Closure only gives a warning on HHVM while
756 // it throws an Exception on Zend.
757 // Workaround for https://github.com/facebook/hhvm/issues/6206
758 $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
759 } else {
760 try {
761 $this->mwGlobals[$globalKey] = unserialize( serialize( $GLOBALS[$globalKey] ) );
762 } catch ( Exception $e ) {
763 $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
764 }
765 }
766 }
767 }
768 }
769
776 private function containsClosure( $var, $maxDepth = 15 ) {
777 if ( $var instanceof Closure ) {
778 return true;
779 }
780 if ( !is_array( $var ) || $maxDepth === 0 ) {
781 return false;
782 }
783
784 foreach ( $var as $value ) {
785 if ( $this->containsClosure( $value, $maxDepth - 1 ) ) {
786 return true;
787 }
788 }
789 return false;
790 }
791
807 protected function mergeMwGlobalArrayValue( $name, $values ) {
808 if ( !isset( $GLOBALS[$name] ) ) {
809 $merged = $values;
810 } else {
811 if ( !is_array( $GLOBALS[$name] ) ) {
812 throw new MWException( "MW global $name is not an array." );
813 }
814
815 // NOTE: do not use array_merge, it screws up for numeric keys.
816 $merged = $GLOBALS[$name];
817 foreach ( $values as $k => $v ) {
818 $merged[$k] = $v;
819 }
820 }
821
822 $this->setMwGlobals( $name, $merged );
823 }
824
839 protected function overrideMwServices( Config $configOverrides = null, array $services = [] ) {
840 if ( !$configOverrides ) {
841 $configOverrides = new HashConfig();
842 }
843
844 $oldInstance = MediaWikiServices::getInstance();
845 $oldConfigFactory = $oldInstance->getConfigFactory();
846 $oldLoadBalancerFactory = $oldInstance->getDBLoadBalancerFactory();
847
848 $testConfig = self::makeTestConfig( null, $configOverrides );
849 $newInstance = new MediaWikiServices( $testConfig );
850
851 // Load the default wiring from the specified files.
852 // NOTE: this logic mirrors the logic in MediaWikiServices::newInstance.
853 $wiringFiles = $testConfig->get( 'ServiceWiringFiles' );
854 $newInstance->loadWiringFiles( $wiringFiles );
855
856 // Provide a traditional hook point to allow extensions to configure services.
857 Hooks::run( 'MediaWikiServices', [ $newInstance ] );
858
859 foreach ( $services as $name => $callback ) {
860 $newInstance->redefineService( $name, $callback );
861 }
862
863 self::installTestServices(
864 $oldConfigFactory,
865 $oldLoadBalancerFactory,
866 $newInstance
867 );
868 MediaWikiServices::forceGlobalInstance( $newInstance );
869
870 return $newInstance;
871 }
872
877 public function setUserLang( $lang ) {
878 RequestContext::getMain()->setLanguage( $lang );
879 $this->setMwGlobals( 'wgLang', RequestContext::getMain()->getLanguage() );
880 }
881
886 public function setContentLang( $lang ) {
887 if ( $lang instanceof Language ) {
888 $langCode = $lang->getCode();
889 $langObj = $lang;
890 } else {
891 $langCode = $lang;
892 $langObj = Language::factory( $langCode );
893 }
894 $this->setMwGlobals( [
895 'wgLanguageCode' => $langCode,
896 'wgContLang' => $langObj,
897 ] );
898 }
899
906 protected function setLogger( $channel, LoggerInterface $logger ) {
907 // TODO: Once loggers are managed by MediaWikiServices, use
908 // overrideMwServices() to set loggers.
909
910 $provider = LoggerFactory::getProvider();
911 $wrappedProvider = TestingAccessWrapper::newFromObject( $provider );
912 $singletons = $wrappedProvider->singletons;
913 if ( $provider instanceof MonologSpi ) {
914 if ( !isset( $this->loggers[$channel] ) ) {
915 $this->loggers[$channel] = isset( $singletons['loggers'][$channel] )
916 ? $singletons['loggers'][$channel] : null;
917 }
918 $singletons['loggers'][$channel] = $logger;
919 } elseif ( $provider instanceof LegacySpi ) {
920 if ( !isset( $this->loggers[$channel] ) ) {
921 $this->loggers[$channel] = isset( $singletons[$channel] ) ? $singletons[$channel] : null;
922 }
923 $singletons[$channel] = $logger;
924 } else {
925 throw new LogicException( __METHOD__ . ': setting a logger for ' . get_class( $provider )
926 . ' is not implemented' );
927 }
928 $wrappedProvider->singletons = $singletons;
929 }
930
935 private function restoreLoggers() {
936 $provider = LoggerFactory::getProvider();
937 $wrappedProvider = TestingAccessWrapper::newFromObject( $provider );
938 $singletons = $wrappedProvider->singletons;
939 foreach ( $this->loggers as $channel => $logger ) {
940 if ( $provider instanceof MonologSpi ) {
941 if ( $logger === null ) {
942 unset( $singletons['loggers'][$channel] );
943 } else {
944 $singletons['loggers'][$channel] = $logger;
945 }
946 } elseif ( $provider instanceof LegacySpi ) {
947 if ( $logger === null ) {
948 unset( $singletons[$channel] );
949 } else {
950 $singletons[$channel] = $logger;
951 }
952 }
953 }
954 $wrappedProvider->singletons = $singletons;
955 $this->loggers = [];
956 }
957
962 public function dbPrefix() {
963 return $this->db->getType() == 'oracle' ? self::ORA_DB_PREFIX : self::DB_PREFIX;
964 }
965
970 public function needsDB() {
971 # if the test says it uses database tables, it needs the database
972 if ( $this->tablesUsed ) {
973 return true;
974 }
975
976 # if the test says it belongs to the Database group, it needs the database
977 $rc = new ReflectionClass( $this );
978 if ( preg_match( '/@group +Database/im', $rc->getDocComment() ) ) {
979 return true;
980 }
981
982 return false;
983 }
984
996 protected function insertPage(
997 $pageName,
998 $text = 'Sample page for unit test.',
999 $namespace = null
1000 ) {
1001 if ( is_string( $pageName ) ) {
1002 $title = Title::newFromText( $pageName, $namespace );
1003 } else {
1004 $title = $pageName;
1005 }
1006
1007 $user = static::getTestSysop()->getUser();
1008 $comment = __METHOD__ . ': Sample page for unit test.';
1009
1010 // Avoid memory leak...?
1011 // LinkCache::singleton()->clear();
1012 // Maybe. But doing this absolutely breaks $title->isRedirect() when called during unit tests....
1013
1014 $page = WikiPage::factory( $title );
1015 $page->doEditContent( ContentHandler::makeContent( $text, $title ), $comment, 0, false, $user );
1016
1017 return [
1018 'title' => $title,
1019 'id' => $page->getId(),
1020 ];
1021 }
1022
1038 public function addDBDataOnce() {
1039 }
1040
1050 public function addDBData() {
1051 }
1052
1053 private function addCoreDBData() {
1054 if ( $this->db->getType() == 'oracle' ) {
1055 # Insert 0 user to prevent FK violations
1056 # Anonymous user
1057 if ( !$this->db->selectField( 'user', '1', [ 'user_id' => 0 ] ) ) {
1058 $this->db->insert( 'user', [
1059 'user_id' => 0,
1060 'user_name' => 'Anonymous' ], __METHOD__, [ 'IGNORE' ] );
1061 }
1062
1063 # Insert 0 page to prevent FK violations
1064 # Blank page
1065 if ( !$this->db->selectField( 'page', '1', [ 'page_id' => 0 ] ) ) {
1066 $this->db->insert( 'page', [
1067 'page_id' => 0,
1068 'page_namespace' => 0,
1069 'page_title' => ' ',
1070 'page_restrictions' => null,
1071 'page_is_redirect' => 0,
1072 'page_is_new' => 0,
1073 'page_random' => 0,
1074 'page_touched' => $this->db->timestamp(),
1075 'page_latest' => 0,
1076 'page_len' => 0 ], __METHOD__, [ 'IGNORE' ] );
1077 }
1078 }
1079
1080 User::resetIdByNameCache();
1081
1082 // Make sysop user
1083 $user = static::getTestSysop()->getUser();
1084
1085 // Make 1 page with 1 revision
1086 $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
1087 if ( $page->getId() == 0 ) {
1088 $page->doEditContent(
1089 new WikitextContent( 'UTContent' ),
1090 'UTPageSummary',
1092 false,
1093 $user
1094 );
1095 // an edit always attempt to purge backlink links such as history
1096 // pages. That is unneccessary.
1097 JobQueueGroup::singleton()->get( 'htmlCacheUpdate' )->delete();
1098 // WikiPages::doEditUpdates randomly adds RC purges
1099 JobQueueGroup::singleton()->get( 'recentChangesUpdate' )->delete();
1100
1101 // doEditContent() probably started the session via
1102 // User::loadFromSession(). Close it now.
1103 if ( session_id() !== '' ) {
1104 session_write_close();
1105 session_id( '' );
1106 }
1107 }
1108 }
1109
1117 public static function teardownTestDB() {
1119
1120 if ( !self::$dbSetup ) {
1121 return;
1122 }
1123
1124 Hooks::run( 'UnitTestsBeforeDatabaseTeardown' );
1125
1126 foreach ( $wgJobClasses as $type => $class ) {
1127 // Delete any jobs under the clone DB (or old prefix in other stores)
1128 JobQueueGroup::singleton()->get( $type )->delete();
1129 }
1130
1131 CloneDatabase::changePrefix( self::$oldTablePrefix );
1132
1133 self::$oldTablePrefix = false;
1134 self::$dbSetup = false;
1135 }
1136
1150 protected static function setupDatabaseWithTestPrefix( IMaintainableDatabase $db, $prefix ) {
1151 $tablesCloned = self::listTables( $db );
1152 $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix );
1153 $dbClone->useTemporaryTables( self::$useTemporaryTables );
1154
1155 if ( ( $db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
1156 CloneDatabase::changePrefix( $prefix );
1157
1158 return false;
1159 } else {
1160 $dbClone->cloneTableStructure();
1161 return true;
1162 }
1163 }
1164
1168 public function setupAllTestDBs() {
1170
1171 self::$oldTablePrefix = $wgDBprefix;
1172
1173 $testPrefix = $this->dbPrefix();
1174
1175 // switch to a temporary clone of the database
1176 self::setupTestDB( $this->db, $testPrefix );
1177
1178 if ( self::isUsingExternalStoreDB() ) {
1179 self::setupExternalStoreTestDBs( $testPrefix );
1180 }
1181 }
1182
1204 public static function setupTestDB( Database $db, $prefix ) {
1205 if ( self::$dbSetup ) {
1206 return;
1207 }
1208
1209 if ( $db->tablePrefix() === $prefix ) {
1210 throw new MWException(
1211 'Cannot run unit tests, the database prefix is already "' . $prefix . '"' );
1212 }
1213
1214 // TODO: the below should be re-written as soon as LBFactory, LoadBalancer,
1215 // and Database no longer use global state.
1216
1217 self::$dbSetup = true;
1218
1219 if ( !self::setupDatabaseWithTestPrefix( $db, $prefix ) ) {
1220 return;
1221 }
1222
1223 // Assuming this isn't needed for External Store database, and not sure if the procedure
1224 // would be available there.
1225 if ( $db->getType() == 'oracle' ) {
1226 $db->query( 'BEGIN FILL_WIKI_INFO; END;' );
1227 }
1228
1229 Hooks::run( 'UnitTestsAfterDatabaseSetup', [ $db, $prefix ] );
1230 }
1231
1237 protected static function setupExternalStoreTestDBs( $testPrefix ) {
1238 $connections = self::getExternalStoreDatabaseConnections();
1239 foreach ( $connections as $dbw ) {
1240 // Hack: cloneTableStructure sets $wgDBprefix to the unit test
1241 // prefix,. Even though listTables now uses tablePrefix, that
1242 // itself is populated from $wgDBprefix by default.
1243
1244 // We have to set it back, or we won't find the original 'blobs'
1245 // table to copy.
1246
1247 $dbw->tablePrefix( self::$oldTablePrefix );
1248 self::setupDatabaseWithTestPrefix( $dbw, $testPrefix );
1249 }
1250 }
1251
1258 protected static function getExternalStoreDatabaseConnections() {
1260
1262 $externalStoreDB = ExternalStore::getStoreObject( 'DB' );
1263 $defaultArray = (array)$wgDefaultExternalStore;
1264 $dbws = [];
1265 foreach ( $defaultArray as $url ) {
1266 if ( strpos( $url, 'DB://' ) === 0 ) {
1267 list( $proto, $cluster ) = explode( '://', $url, 2 );
1268 // Avoid getMaster() because setupDatabaseWithTestPrefix()
1269 // requires Database instead of plain DBConnRef/IDatabase
1270 $dbws[] = $externalStoreDB->getMaster( $cluster );
1271 }
1272 }
1273
1274 return $dbws;
1275 }
1276
1282 protected static function isUsingExternalStoreDB() {
1284 if ( !$wgDefaultExternalStore ) {
1285 return false;
1286 }
1287
1288 $defaultArray = (array)$wgDefaultExternalStore;
1289 foreach ( $defaultArray as $url ) {
1290 if ( strpos( $url, 'DB://' ) === 0 ) {
1291 return true;
1292 }
1293 }
1294
1295 return false;
1296 }
1297
1304 private function resetDB( $db, $tablesUsed ) {
1305 if ( $db ) {
1306 $userTables = [ 'user', 'user_groups', 'user_properties' ];
1307 $pageTables = [ 'page', 'revision', 'ip_changes', 'revision_comment_temp', 'comment' ];
1308 $coreDBDataTables = array_merge( $userTables, $pageTables );
1309
1310 // If any of the user or page tables were marked as used, we should clear all of them.
1311 if ( array_intersect( $tablesUsed, $userTables ) ) {
1312 $tablesUsed = array_unique( array_merge( $tablesUsed, $userTables ) );
1314 }
1315 if ( array_intersect( $tablesUsed, $pageTables ) ) {
1316 $tablesUsed = array_unique( array_merge( $tablesUsed, $pageTables ) );
1317 }
1318
1319 $truncate = in_array( $db->getType(), [ 'oracle', 'mysql' ] );
1320 foreach ( $tablesUsed as $tbl ) {
1321 // TODO: reset interwiki table to its original content.
1322 if ( $tbl == 'interwiki' ) {
1323 continue;
1324 }
1325
1326 if ( $truncate ) {
1327 $db->query( 'TRUNCATE TABLE ' . $db->tableName( $tbl ), __METHOD__ );
1328 } else {
1329 $db->delete( $tbl, '*', __METHOD__ );
1330 }
1331
1332 if ( $tbl === 'page' ) {
1333 // Forget about the pages since they don't
1334 // exist in the DB.
1335 LinkCache::singleton()->clear();
1336 }
1337 }
1338
1339 if ( array_intersect( $tablesUsed, $coreDBDataTables ) ) {
1340 // Re-add core DB data that was deleted
1341 $this->addCoreDBData();
1342 }
1343 }
1344 }
1345
1355 public function __call( $func, $args ) {
1356 static $compatibility = [
1357 'createMock' => 'createMock2',
1358 ];
1359
1360 if ( isset( $compatibility[$func] ) ) {
1361 return call_user_func_array( [ $this, $compatibility[$func] ], $args );
1362 } else {
1363 throw new MWException( "Called non-existent $func method on " . static::class );
1364 }
1365 }
1366
1374 private function createMock2( $originalClassName ) {
1375 return $this->getMockBuilder( $originalClassName )
1376 ->disableOriginalConstructor()
1377 ->disableOriginalClone()
1378 ->disableArgumentCloning()
1379 // New in phpunit-mock-objects 3.2 (phpunit 5.4.0)
1380 // ->disallowMockingUnknownTypes()
1381 ->getMock();
1382 }
1383
1384 private static function unprefixTable( &$tableName, $ind, $prefix ) {
1385 $tableName = substr( $tableName, strlen( $prefix ) );
1386 }
1387
1388 private static function isNotUnittest( $table ) {
1389 return strpos( $table, 'unittest_' ) !== 0;
1390 }
1391
1399 public static function listTables( IMaintainableDatabase $db ) {
1400 $prefix = $db->tablePrefix();
1401 $tables = $db->listTables( $prefix, __METHOD__ );
1402
1403 if ( $db->getType() === 'mysql' ) {
1404 static $viewListCache = null;
1405 if ( $viewListCache === null ) {
1406 $viewListCache = $db->listViews( null, __METHOD__ );
1407 }
1408 // T45571: cannot clone VIEWs under MySQL
1409 $tables = array_diff( $tables, $viewListCache );
1410 }
1411 array_walk( $tables, [ __CLASS__, 'unprefixTable' ], $prefix );
1412
1413 // Don't duplicate test tables from the previous fataled run
1414 $tables = array_filter( $tables, [ __CLASS__, 'isNotUnittest' ] );
1415
1416 if ( $db->getType() == 'sqlite' ) {
1417 $tables = array_flip( $tables );
1418 // these are subtables of searchindex and don't need to be duped/dropped separately
1419 unset( $tables['searchindex_content'] );
1420 unset( $tables['searchindex_segdir'] );
1421 unset( $tables['searchindex_segments'] );
1422 $tables = array_flip( $tables );
1423 }
1424
1425 return $tables;
1426 }
1427
1432 protected function checkDbIsSupported() {
1433 if ( !in_array( $this->db->getType(), $this->supportedDBs ) ) {
1434 throw new MWException( $this->db->getType() . " is not currently supported for unit testing." );
1435 }
1436 }
1437
1443 public function getCliArg( $offset ) {
1444 if ( isset( PHPUnitMaintClass::$additionalOptions[$offset] ) ) {
1446 }
1447
1448 return null;
1449 }
1450
1456 public function setCliArg( $offset, $value ) {
1458 }
1459
1467 public function hideDeprecated( $function ) {
1468 MediaWiki\suppressWarnings();
1469 wfDeprecated( $function );
1470 MediaWiki\restoreWarnings();
1471 }
1472
1491 protected function assertSelect( $table, $fields, $condition, array $expectedRows ) {
1492 if ( !$this->needsDB() ) {
1493 throw new MWException( 'When testing database state, the test cases\'s needDB()' .
1494 ' method should return true. Use @group Database or $this->tablesUsed.' );
1495 }
1496
1497 $db = wfGetDB( DB_REPLICA );
1498
1499 $res = $db->select( $table, $fields, $condition, wfGetCaller(), [ 'ORDER BY' => $fields ] );
1500 $this->assertNotEmpty( $res, "query failed: " . $db->lastError() );
1501
1502 $i = 0;
1503
1504 foreach ( $expectedRows as $expected ) {
1505 $r = $res->fetchRow();
1506 self::stripStringKeys( $r );
1507
1508 $i += 1;
1509 $this->assertNotEmpty( $r, "row #$i missing" );
1510
1511 $this->assertEquals( $expected, $r, "row #$i mismatches" );
1512 }
1513
1514 $r = $res->fetchRow();
1515 self::stripStringKeys( $r );
1516
1517 $this->assertFalse( $r, "found extra row (after #$i)" );
1518 }
1519
1531 protected function arrayWrap( array $elements ) {
1532 return array_map(
1533 function ( $element ) {
1534 return [ $element ];
1535 },
1536 $elements
1537 );
1538 }
1539
1552 protected function assertArrayEquals( array $expected, array $actual,
1553 $ordered = false, $named = false
1554 ) {
1555 if ( !$ordered ) {
1556 $this->objectAssociativeSort( $expected );
1557 $this->objectAssociativeSort( $actual );
1558 }
1559
1560 if ( !$named ) {
1561 $expected = array_values( $expected );
1562 $actual = array_values( $actual );
1563 }
1564
1565 call_user_func_array(
1566 [ $this, 'assertEquals' ],
1567 array_merge( [ $expected, $actual ], array_slice( func_get_args(), 4 ) )
1568 );
1569 }
1570
1583 protected function assertHTMLEquals( $expected, $actual, $msg = '' ) {
1584 $expected = str_replace( '>', ">\n", $expected );
1585 $actual = str_replace( '>', ">\n", $actual );
1586
1587 $this->assertEquals( $expected, $actual, $msg );
1588 }
1589
1597 protected function objectAssociativeSort( array &$array ) {
1598 uasort(
1599 $array,
1600 function ( $a, $b ) {
1601 return serialize( $a ) > serialize( $b ) ? 1 : -1;
1602 }
1603 );
1604 }
1605
1615 protected static function stripStringKeys( &$r ) {
1616 if ( !is_array( $r ) ) {
1617 return;
1618 }
1619
1620 foreach ( $r as $k => $v ) {
1621 if ( is_string( $k ) ) {
1622 unset( $r[$k] );
1623 }
1624 }
1625 }
1626
1640 protected function assertTypeOrValue( $type, $actual, $value = false, $message = '' ) {
1641 if ( $actual === $value ) {
1642 $this->assertTrue( true, $message );
1643 } else {
1644 $this->assertType( $type, $actual, $message );
1645 }
1646 }
1647
1659 protected function assertType( $type, $actual, $message = '' ) {
1660 if ( class_exists( $type ) || interface_exists( $type ) ) {
1661 $this->assertInstanceOf( $type, $actual, $message );
1662 } else {
1663 $this->assertInternalType( $type, $actual, $message );
1664 }
1665 }
1666
1676 protected function isWikitextNS( $ns ) {
1678
1679 if ( isset( $wgNamespaceContentModels[$ns] ) ) {
1681 }
1682
1683 return true;
1684 }
1685
1693 protected function getDefaultWikitextNS() {
1695
1696 static $wikitextNS = null; // this is not going to change
1697 if ( $wikitextNS !== null ) {
1698 return $wikitextNS;
1699 }
1700
1701 // quickly short out on most common case:
1702 if ( !isset( $wgNamespaceContentModels[NS_MAIN] ) ) {
1703 return NS_MAIN;
1704 }
1705
1706 // NOTE: prefer content namespaces
1707 $namespaces = array_unique( array_merge(
1708 MWNamespace::getContentNamespaces(),
1709 [ NS_MAIN, NS_HELP, NS_PROJECT ], // prefer these
1710 MWNamespace::getValidNamespaces()
1711 ) );
1712
1713 $namespaces = array_diff( $namespaces, [
1714 NS_FILE, NS_CATEGORY, NS_MEDIAWIKI, NS_USER // don't mess with magic namespaces
1715 ] );
1716
1717 $talk = array_filter( $namespaces, function ( $ns ) {
1718 return MWNamespace::isTalk( $ns );
1719 } );
1720
1721 // prefer non-talk pages
1722 $namespaces = array_diff( $namespaces, $talk );
1723 $namespaces = array_merge( $namespaces, $talk );
1724
1725 // check default content model of each namespace
1726 foreach ( $namespaces as $ns ) {
1727 if ( !isset( $wgNamespaceContentModels[$ns] ) ||
1729 ) {
1730 $wikitextNS = $ns;
1731
1732 return $wikitextNS;
1733 }
1734 }
1735
1736 // give up
1737 // @todo Inside a test, we could skip the test as incomplete.
1738 // But frequently, this is used in fixture setup.
1739 throw new MWException( "No namespace defaults to wikitext!" );
1740 }
1741
1748 protected function markTestSkippedIfNoDiff3() {
1750
1751 # This check may also protect against code injection in
1752 # case of broken installations.
1753 MediaWiki\suppressWarnings();
1754 $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 );
1755 MediaWiki\restoreWarnings();
1756
1757 if ( !$haveDiff3 ) {
1758 $this->markTestSkipped( "Skip test, since diff3 is not configured" );
1759 }
1760 }
1761
1770 protected function checkPHPExtension( $extName ) {
1771 $loaded = extension_loaded( $extName );
1772 if ( !$loaded ) {
1773 $this->markTestSkipped( "PHP extension '$extName' is not loaded, skipping." );
1774 }
1775
1776 return $loaded;
1777 }
1778
1793 protected function assertValidHtmlSnippet( $html ) {
1794 $html = '<!DOCTYPE html><html><head><title>test</title></head><body>' . $html . '</body></html>';
1796 }
1797
1809 protected function assertValidHtmlDocument( $html ) {
1810 // Note: we only validate if the tidy PHP extension is available.
1811 // In case wgTidyInternal is false, MWTidy would fall back to the command line version
1812 // of tidy. In that case however, we can not reliably detect whether a failing validation
1813 // is due to malformed HTML, or caused by tidy not being installed as a command line tool.
1814 // That would cause all HTML assertions to fail on a system that has no tidy installed.
1815 if ( !$GLOBALS['wgTidyInternal'] || !MWTidy::isEnabled() ) {
1816 $this->markTestSkipped( 'Tidy extension not installed' );
1817 }
1818
1819 $errorBuffer = '';
1820 MWTidy::checkErrors( $html, $errorBuffer );
1821 $allErrors = preg_split( '/[\r\n]+/', $errorBuffer );
1822
1823 // Filter Tidy warnings which aren't useful for us.
1824 // Tidy eg. often cries about parameters missing which have actually
1825 // been deprecated since HTML4, thus we should not care about them.
1826 $errors = preg_grep(
1827 '/^(.*Warning: (trimming empty|.* lacks ".*?" attribute).*|\s*)$/m',
1828 $allErrors, PREG_GREP_INVERT
1829 );
1830
1831 $this->assertEmpty( $errors, implode( "\n", $errors ) );
1832 }
1833
1839 public static function wfResetOutputBuffersBarrier( $buffer ) {
1840 return $buffer;
1841 }
1842
1850 protected function setTemporaryHook( $hookName, $handler ) {
1851 $this->mergeMwGlobalArrayValue( 'wgHooks', [ $hookName => [ $handler ] ] );
1852 }
1853
1863 protected function assertFileContains(
1864 $fileName,
1865 $actualData,
1866 $createIfMissing = true,
1867 $msg = ''
1868 ) {
1869 if ( $createIfMissing ) {
1870 if ( !file_exists( $fileName ) ) {
1871 file_put_contents( $fileName, $actualData );
1872 $this->markTestSkipped( 'Data file $fileName does not exist' );
1873 }
1874 } else {
1875 self::assertFileExists( $fileName );
1876 }
1877 self::assertEquals( file_get_contents( $fileName ), $actualData, $msg );
1878 }
1879}
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
serialize()
unserialize( $serialized)
$GLOBALS['IP']
$wgDBprefix
Table name prefix.
$wgSQLMode
SQL Mode - default is turning off all modes, including strict, if set.
array $wgDefaultExternalStore
The place to put new revisions, false to put them in the local text table.
$wgDiff3
Path to the GNU diff3 utility.
$wgJobClasses
Maps jobs to their handlers; extensions can add to this to provide custom jobs.
$wgNamespaceContentModels
Associative array mapping namespace IDs to the name of the content model pages in that namespace shou...
wfTempDir()
Tries to get the system directory for temporary files.
wfRecursiveRemoveDir( $dir)
Remove a directory and all its content.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfGetCaller( $level=2)
Get the name of the function which called this function wfGetCaller( 1 ) is the function with the wfG...
wfMkdirParents( $dir, $mode=null, $caller=null)
Make directory, and make all parent directories if they don't exist.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
if(! $wgDBerrorLogTZ) $wgRequest
Definition Setup.php:662
if( $line===false) $args
Definition cdb.php:63
static changePrefix( $prefix)
Change the table prefix on all open DB connections/.
Factory class to create Config objects.
makeConfig( $name)
Create a given Config using the registered callback for $name.
static makeContent( $text, Title $title=null, $modelId=null, $format=null)
Convenience function for creating a Content object from a given textual representation.
static getStoreObject( $proto, array $params=[])
Get an external store object of the given type, with the given parameters.
WebRequest clone which takes values from a provided array.
static destroySingleton()
Destroy the singleton instance.
Accesses configuration settings from $GLOBALS.
A Config instance which stores all settings as a member variable.
static destroySingletons()
Destroy the singleton instances.
static singleton( $wiki=false)
Internationalisation code.
Definition Language.php:35
MediaWiki exception.
static checkErrors( $text, &$errorStr=null)
Check HTML for errors, used if $wgValidateAllHtml = true.
Definition MWTidy.php:63
static isEnabled()
Definition MWTidy.php:79
static setLBFactoryTriggers(LBFactory $LBFactory)
setupAllTestDBs()
Set up all test DBs.
int $phpErrorLevel
Original value of PHP's error_reporting setting.
getDefaultWikitextNS()
Returns the ID of a namespace that defaults to Wikitext.
containsClosure( $var, $maxDepth=15)
static TestUser[] $users
static stripStringKeys(&$r)
Utility function for eliminating all string keys from an array.
Database $db
Primary database.
getNewTempFile()
Obtains a new temporary file name.
static isNotUnittest( $table)
static setupExternalStoreTestDBs( $testPrefix)
Clones the External Store database(s) for testing.
static getMutableTestUser( $groups=[])
Convenience method for getting a mutable test user.
insertPage( $pageName, $text='Sample page for unit test.', $namespace=null)
Insert a new page.
static getTestSysop()
Convenience method for getting an immutable admin test user.
restoreLoggers()
Restores loggers replaced by setLogger().
overrideMwServices(Config $configOverrides=null, array $services=[])
Stashes the global instance of MediaWikiServices, and installs a new one, allowing test cases to over...
static installTestServices(ConfigFactory $oldConfigFactory, LBFactory $oldLoadBalancerFactory, MediaWikiServices $newServices)
static MediaWikiServices null $serviceLocator
The service locator created by prepareServices().
assertValidHtmlSnippet( $html)
Asserts that the given string is a valid HTML snippet.
setLogger( $channel, LoggerInterface $logger)
Sets the logger for a specified channel, for the duration of the test.
static getExternalStoreDatabaseConnections()
Gets master database connections for all of the ExternalStoreDB stores configured in $wgDefaultExtern...
assertType( $type, $actual, $message='')
Asserts the type of the provided value.
run(PHPUnit_Framework_TestResult $result=null)
static resetGlobalServices(Config $bootstrapConfig=null)
Reset global services, and install testing environment.
mergeMwGlobalArrayValue( $name, $values)
Merges the given values into a MW global array variable.
doLightweightServiceReset()
Resets some well known services that typically have state that may interfere with unit tests.
static setupTestDB(Database $db, $prefix)
Creates an empty skeleton of the wiki database by cloning its structure to equivalent tables using th...
static canShallowCopy( $value)
Check if we can back up a value by performing a shallow copy.
setCliArg( $offset, $value)
setMwGlobals( $pairs, $value=null)
Sets a global, maintaining a stashed version of the previous global to be restored in tearDown.
testMediaWikiTestCaseParentSetupCalled()
Make sure MediaWikiTestCase extending classes have called their parent setUp method.
array $mwGlobals
Holds original values of MediaWiki configuration settings to be restored in tearDown().
array $tmpFiles
Holds the paths of temporary files/directories created through getNewTempFile, and getNewTempDirector...
assertValidHtmlDocument( $html)
Asserts that the given string is valid HTML document.
hideDeprecated( $function)
Don't throw a warning if $function is deprecated and called later.
static setupDatabaseWithTestPrefix(IMaintainableDatabase $db, $prefix)
Setups a database with the given prefix.
__construct( $name=null, array $data=[], $dataName='')
checkPHPExtension( $extName)
Check if $extName is a loaded PHP extension, will skip the test whenever it is not loaded.
static listTables(IMaintainableDatabase $db)
resetDB( $db, $tablesUsed)
Empty all tables so they can be repopulated for tests.
static makeTestConfig(Config $baseConfig=null, Config $customOverrides=null)
Create a config suitable for testing, based on a base config, default overrides, and custom overrides...
static teardownTestDB()
Restores MediaWiki to using the table set (table prefix) it was using before setupTestDB() was called...
static wfResetOutputBuffersBarrier( $buffer)
Used as a marker to prevent wfResetOutputBuffers from breaking PHPUnit.
$called
$called tracks whether the setUp and tearDown method has been called.
createMock2( $originalClassName)
Return a test double for the specified class.
setService( $name, $object)
Sets a service, maintaining a stashed version of the previous service to be restored in tearDown.
const DB_PREFIX
Table name prefixes.
static getTestUser( $groups=[])
Convenience method for getting an immutable test user.
static makeTestConfigFactoryInstantiator(ConfigFactory $oldFactory, array $configurations)
isWikitextNS( $ns)
Returns true if the given namespace defaults to Wikitext according to $wgNamespaceContentModels.
getNewTempDirectory()
obtains a new temporary directory
static isUsingExternalStoreDB()
Check whether ExternalStoreDB is being used.
array $mwGlobalsToUnset
Holds list of MediaWiki configuration settings to be unset in tearDown().
objectAssociativeSort(array &$array)
Does an associative sort that works for objects.
setTemporaryHook( $hookName, $handler)
Create a temporary hook handler which will be reset by tearDown.
LoggerInterface[] $loggers
Holds original loggers which have been replaced by setLogger()
static prepareServices(Config $bootstrapConfig)
Prepare service configuration for unit testing.
stashMwGlobals( $globalKeys)
Stashes the global, will be restored in tearDown()
static unprefixTable(&$tableName, $ind, $prefix)
assertHTMLEquals( $expected, $actual, $msg='')
Put each HTML element on its own line and then equals() the results.
assertFileContains( $fileName, $actualData, $createIfMissing=true, $msg='')
Check whether file contains given data.
assertSelect( $table, $fields, $condition, array $expectedRows)
Asserts that the given database query yields the rows given by $expectedRows.
arrayWrap(array $elements)
Utility method taking an array of elements and wrapping each element in its own array.
assertArrayEquals(array $expected, array $actual, $ordered=false, $named=false)
Assert that two arrays are equal.
markTestSkippedIfNoDiff3()
Check, if $wgDiff3 is set and ready to merge Will mark the calling test as skipped,...
assertTypeOrValue( $type, $actual, $value=false, $message='')
Asserts that the provided variable is of the specified internal type or equals the $value argument.
LoggerFactory service provider that creates LegacyLogger instances.
Definition LegacySpi.php:37
PSR-3 logger instance factory.
LoggerFactory service provider that creates loggers implemented by Monolog.
MediaWikiServices is the service locator for the application scope of MediaWiki.
getBootstrapConfig()
Returns the Config object containing the bootstrap configuration.
resetServiceForTesting( $name, $destroy=true)
Resets the given service for testing purposes.
redefineService( $name, callable $instantiator)
Replace an already defined service.
Provides a fallback sequence for Config objects.
static $additionalOptions
Definition phpunit.php:18
static resetMain()
Resets singleton returned by getMain().
static getMain()
Static methods.
static clear()
Clear the registry.
static getMutableTestUser( $testName, $groups=[])
Get a TestUser object that the caller may modify.
static getImmutableTestUser( $groups=[])
Get a TestUser object that the caller may not modify.
Wraps the user object, so we can also retain full access to properties like password if we log in via...
Definition TestUser.php:7
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:51
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition WikiPage.php:121
Relational database abstraction object.
Definition Database.php:45
tablePrefix( $prefix=null)
Get/set the table prefix.
Definition Database.php:480
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
tableName( $name, $format='quoted')
Format a table name ready for use in constructing an SQL query.
delete( $table, $conds, $fname=__METHOD__)
DELETE query wrapper.
query( $sql, $fname=__METHOD__, $tempIgnore=false)
Run an SQL query and return the result.
Definition Database.php:888
Content object for wiki text pages.
$res
Definition database.txt:21
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
when a variable name is used in a it is silently declared as a new local masking the global
Definition design.txt:95
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
const NS_HELP
Definition Defines.php:77
const NS_USER
Definition Defines.php:67
const NS_FILE
Definition Defines.php:71
const CACHE_NONE
Definition Defines.php:103
const NS_MAIN
Definition Defines.php:65
const NS_MEDIAWIKI
Definition Defines.php:73
const CACHE_ACCEL
Definition Defines.php:106
const EDIT_SUPPRESS_RC
Definition Defines.php:156
const CONTENT_MODEL_WIKITEXT
Definition Defines.php:236
const CACHE_MEMCACHED
Definition Defines.php:105
const CACHE_DB
Definition Defines.php:104
const NS_PROJECT
Definition Defines.php:69
const NS_CATEGORY
Definition Defines.php:79
const EDIT_NEW
Definition Defines.php:153
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action or null $user:User who performed the tagging when the tagging is subsequent to the action or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy:boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition hooks.txt:1245
the array() calling protocol came about after MediaWiki 1.4rc1.
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message. Please note the header message cannot receive/use parameters. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item. Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page. Return false to stop further processing of the tag $reader:XMLReader object & $pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision. Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag. Return false to stop further processing of the tag $reader:XMLReader object 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload. Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports. & $fullInterwikiPrefix:Interwiki prefix, may contain colons. & $pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable. Can be used to lazy-load the import sources list. & $importSources:The value of $wgImportSources. Modify as necessary. See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page. $context:IContextSource object & $pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect. & $title:Title object for the current page & $request:WebRequest & $ignoreRedirect:boolean to skip redirect check & $target:Title/string of redirect target & $article:Article object 'InternalParseBeforeLinks':during Parser 's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InternalParseBeforeSanitize':during Parser 's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings. Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not. Return true without providing an interwiki to continue interwiki search. $prefix:interwiki prefix we are looking for. & $iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user 's email has been invalidated successfully. $user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification. Callee may modify $url and $query, URL will be constructed as $url . $query & $url:URL to index.php & $query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) & $article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() & $ip:IP being check & $result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from & $allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn 't match your organization. $addr:The e-mail address entered by the user & $result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user & $result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we 're looking for a messages file for & $file:The messages file path, you can override this to change the location. 'LanguageGetMagic':DEPRECATED! Use $magicWords in a file listed in $wgExtensionMessagesFiles instead. Use this to define synonyms of magic words depending of the language & $magicExtensions:associative array of magic words synonyms $lang:language code(string) 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces. Do not use this hook to add namespaces. Use CanonicalNamespaces for that. & $namespaces:Array of namespaces indexed by their numbers 'LanguageGetSpecialPageAliases':DEPRECATED! Use $specialPageAliases in a file listed in $wgExtensionMessagesFiles instead. Use to define aliases of special pages names depending of the language & $specialPageAliases:associative array of magic words synonyms $lang:language code(string) 'LanguageGetTranslatedLanguageNames':Provide translated language names. & $names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page 's language links. This is called in various places to allow extensions to define the effective language links for a page. $title:The page 's Title. & $links:Array with elements of the form "language:title" in the order that they will be output. & $linkFlags:Associative array mapping prefixed links to arrays of flags. Currently unused, but planned to provide support for marking individual language links in the UI, e.g. for featured articles. 'LanguageSelector':Hook to change the language selector available on a page. $out:The output page. $cssClassName:CSS class name of the language selector. 'LinkBegin':DEPRECATED! Use HtmlPageLinkRendererBegin instead. Used when generating internal and interwiki links in Linker::link(), before processing starts. Return false to skip default processing and return $ret. See documentation for Linker::link() for details on the expected meanings of parameters. $skin:the Skin object $target:the Title that the link is pointing to & $html:the contents that the< a > tag should have(raw HTML) $result
Definition hooks.txt:1963
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist & $tables
Definition hooks.txt:1013
namespace and then decline to actually register it & $namespaces
Definition hooks.txt:932
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:962
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title after the basic globals have been set but before ordinary actions take place or wrap services the preferred way to define a new service is the $wgServiceWiringFiles array $services
Definition hooks.txt:2243
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses & $html
Definition hooks.txt:1983
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:302
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check set to true or false to override the $wgMaxImageArea check result gives extension the possibility to transform it themselves $handler
Definition hooks.txt:901
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account $user
Definition hooks.txt:247
returning false will NOT prevent logging $e
Definition hooks.txt:2146
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition injection.txt:37
listTables( $prefix=null, $fname=__METHOD__)
List all tables on the database.
lastError()
Get a description of the last error.
getType()
Get the type of the DBMS, as it appears in $wgDBtype.
tablePrefix( $prefix=null)
Get/set the table prefix.
Advanced database interface for IDatabase handles that include maintenance methods.
listViews( $prefix=null, $fname=__METHOD__)
Lists all the VIEWs in the database.
$buffer
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:26
if(!isset( $args[0])) $lang