MediaWiki REL1_31
MediaWikiTestCase.php
Go to the documentation of this file.
1<?php
2
7use Psr\Log\LoggerInterface;
12use Wikimedia\TestingAccessWrapper;
13
17abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase {
18
19 use MediaWikiCoversValidator;
20 use PHPUnit4And6Compat;
21
29 private static $serviceLocator = null;
30
43 private $called = [];
44
49 public static $users;
50
57 protected $db;
58
63 protected $tablesUsed = []; // tables with data
64
65 private static $useTemporaryTables = true;
66 private static $reuseDB = false;
67 private static $dbSetup = false;
68 private static $oldTablePrefix = '';
69
76
83 private $tmpFiles = [];
84
91 private $mwGlobals = [];
92
98 private $mwGlobalsToUnset = [];
99
104 private $loggers = [];
105
109 const DB_PREFIX = 'unittest_';
110 const ORA_DB_PREFIX = 'ut_';
111
116 protected $supportedDBs = [
117 'mysql',
118 'sqlite',
119 'postgres',
120 'oracle'
121 ];
122
123 public function __construct( $name = null, array $data = [], $dataName = '' ) {
124 parent::__construct( $name, $data, $dataName );
125
126 $this->backupGlobals = false;
127 $this->backupStaticAttributes = false;
128 }
129
130 public function __destruct() {
131 // Complain if self::setUp() was called, but not self::tearDown()
132 // $this->called['setUp'] will be checked by self::testMediaWikiTestCaseParentSetupCalled()
133 if ( isset( $this->called['setUp'] ) && !isset( $this->called['tearDown'] ) ) {
134 throw new MWException( static::class . "::tearDown() must call parent::tearDown()" );
135 }
136 }
137
138 public static function setUpBeforeClass() {
139 parent::setUpBeforeClass();
140
141 // Get the service locator, and reset services if it's not done already
142 self::$serviceLocator = self::prepareServices( new GlobalVarConfig() );
143 }
144
153 public static function getTestUser( $groups = [] ) {
154 return TestUserRegistry::getImmutableTestUser( $groups );
155 }
156
165 public static function getMutableTestUser( $groups = [] ) {
166 return TestUserRegistry::getMutableTestUser( __CLASS__, $groups );
167 }
168
177 public static function getTestSysop() {
178 return self::getTestUser( [ 'sysop', 'bureaucrat' ] );
179 }
180
200 public static function prepareServices( Config $bootstrapConfig ) {
201 static $services = null;
202
203 if ( !$services ) {
204 $services = self::resetGlobalServices( $bootstrapConfig );
205 }
206 return $services;
207 }
208
223 protected static function resetGlobalServices( Config $bootstrapConfig = null ) {
224 $oldServices = MediaWikiServices::getInstance();
225 $oldConfigFactory = $oldServices->getConfigFactory();
226 $oldLoadBalancerFactory = $oldServices->getDBLoadBalancerFactory();
227
228 $testConfig = self::makeTestConfig( $bootstrapConfig );
229
230 MediaWikiServices::resetGlobalInstance( $testConfig );
231
232 $serviceLocator = MediaWikiServices::getInstance();
233 self::installTestServices(
234 $oldConfigFactory,
235 $oldLoadBalancerFactory,
237 );
238 return $serviceLocator;
239 }
240
250 private static function makeTestConfig(
251 Config $baseConfig = null,
252 Config $customOverrides = null
253 ) {
254 $defaultOverrides = new HashConfig();
255
256 if ( !$baseConfig ) {
257 $baseConfig = MediaWikiServices::getInstance()->getBootstrapConfig();
258 }
259
260 /* Some functions require some kind of caching, and will end up using the db,
261 * which we can't allow, as that would open a new connection for mysql.
262 * Replace with a HashBag. They would not be going to persist anyway.
263 */
264 $hashCache = [ 'class' => HashBagOStuff::class, 'reportDupes' => false ];
265 $objectCaches = [
266 CACHE_DB => $hashCache,
267 CACHE_ACCEL => $hashCache,
268 CACHE_MEMCACHED => $hashCache,
269 'apc' => $hashCache,
270 'apcu' => $hashCache,
271 'wincache' => $hashCache,
272 ] + $baseConfig->get( 'ObjectCaches' );
273
274 $defaultOverrides->set( 'ObjectCaches', $objectCaches );
275 $defaultOverrides->set( 'MainCacheType', CACHE_NONE );
276 $defaultOverrides->set( 'JobTypeConf', [ 'default' => [ 'class' => JobQueueMemory::class ] ] );
277
278 // Use a fast hash algorithm to hash passwords.
279 $defaultOverrides->set( 'PasswordDefault', 'A' );
280
281 $testConfig = $customOverrides
282 ? new MultiConfig( [ $customOverrides, $defaultOverrides, $baseConfig ] )
283 : new MultiConfig( [ $defaultOverrides, $baseConfig ] );
284
285 return $testConfig;
286 }
287
295 private static function installTestServices(
296 ConfigFactory $oldConfigFactory,
297 LBFactory $oldLoadBalancerFactory,
298 MediaWikiServices $newServices
299 ) {
300 // Use bootstrap config for all configuration.
301 // This allows config overrides via global variables to take effect.
302 $bootstrapConfig = $newServices->getBootstrapConfig();
303 $newServices->resetServiceForTesting( 'ConfigFactory' );
304 $newServices->redefineService(
305 'ConfigFactory',
306 self::makeTestConfigFactoryInstantiator(
307 $oldConfigFactory,
308 [ 'main' => $bootstrapConfig ]
309 )
310 );
311 $newServices->resetServiceForTesting( 'DBLoadBalancerFactory' );
312 $newServices->redefineService(
313 'DBLoadBalancerFactory',
314 function ( MediaWikiServices $services ) use ( $oldLoadBalancerFactory ) {
315 return $oldLoadBalancerFactory;
316 }
317 );
318 }
319
326 private static function makeTestConfigFactoryInstantiator(
327 ConfigFactory $oldFactory,
328 array $configurations
329 ) {
330 return function ( MediaWikiServices $services ) use ( $oldFactory, $configurations ) {
331 $factory = new ConfigFactory();
332
333 // clone configurations from $oldFactory that are not overwritten by $configurations
334 $namesToClone = array_diff(
335 $oldFactory->getConfigNames(),
336 array_keys( $configurations )
337 );
338
339 foreach ( $namesToClone as $name ) {
340 $factory->register( $name, $oldFactory->makeConfig( $name ) );
341 }
342
343 foreach ( $configurations as $name => $config ) {
344 $factory->register( $name, $config );
345 }
346
347 return $factory;
348 };
349 }
350
362 private function doLightweightServiceReset() {
363 global $wgRequest;
364
366 ObjectCache::clear();
367 $services = MediaWikiServices::getInstance();
368 $services->resetServiceForTesting( 'MainObjectStash' );
369 $services->resetServiceForTesting( 'LocalServerObjectCache' );
370 $services->getMainWANObjectCache()->clearProcessCache();
372
373 // TODO: move global state into MediaWikiServices
375 if ( session_id() !== '' ) {
376 session_write_close();
377 session_id( '' );
378 }
379
380 $wgRequest = new FauxRequest();
381 MediaWiki\Session\SessionManager::resetCache();
382 }
383
384 public function run( PHPUnit_Framework_TestResult $result = null ) {
385 // Reset all caches between tests.
387
388 $needsResetDB = false;
389
390 if ( !self::$dbSetup || $this->needsDB() ) {
391 // set up a DB connection for this test to use
392
393 self::$useTemporaryTables = !$this->getCliArg( 'use-normal-tables' );
394 self::$reuseDB = $this->getCliArg( 'reuse-db' );
395
396 $this->db = wfGetDB( DB_MASTER );
397
398 $this->checkDbIsSupported();
399
400 if ( !self::$dbSetup ) {
401 $this->setupAllTestDBs();
402 $this->addCoreDBData();
403
404 if ( ( $this->db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
405 $this->resetDB( $this->db, $this->tablesUsed );
406 }
407 }
408
409 // TODO: the DB setup should be done in setUpBeforeClass(), so the test DB
410 // is available in subclass's setUpBeforeClass() and setUp() methods.
411 // This would also remove the need for the HACK that is oncePerClass().
412 if ( $this->oncePerClass() ) {
413 $this->setUpSchema( $this->db );
414 $this->addDBDataOnce();
415 }
416
417 $this->addDBData();
418 $needsResetDB = true;
419 }
420
421 parent::run( $result );
422
423 if ( $needsResetDB ) {
424 $this->resetDB( $this->db, $this->tablesUsed );
425 }
426 }
427
431 private function oncePerClass() {
432 // Remember current test class in the database connection,
433 // so we know when we need to run addData.
434
435 $class = static::class;
436
437 $first = !isset( $this->db->_hasDataForTestClass )
438 || $this->db->_hasDataForTestClass !== $class;
439
440 $this->db->_hasDataForTestClass = $class;
441 return $first;
442 }
443
449 public function usesTemporaryTables() {
450 return self::$useTemporaryTables;
451 }
452
462 protected function getNewTempFile() {
463 $fileName = tempnam( wfTempDir(), 'MW_PHPUnit_' . static::class . '_' );
464 $this->tmpFiles[] = $fileName;
465
466 return $fileName;
467 }
468
479 protected function getNewTempDirectory() {
480 // Starting of with a temporary /file/.
481 $fileName = $this->getNewTempFile();
482
483 // Converting the temporary /file/ to a /directory/
484 // The following is not atomic, but at least we now have a single place,
485 // where temporary directory creation is bundled and can be improved
486 unlink( $fileName );
487 $this->assertTrue( wfMkdirParents( $fileName ) );
488
489 return $fileName;
490 }
491
492 protected function setUp() {
493 parent::setUp();
494 $this->called['setUp'] = true;
495
496 $this->phpErrorLevel = intval( ini_get( 'error_reporting' ) );
497
498 // Cleaning up temporary files
499 foreach ( $this->tmpFiles as $fileName ) {
500 if ( is_file( $fileName ) || ( is_link( $fileName ) ) ) {
501 unlink( $fileName );
502 } elseif ( is_dir( $fileName ) ) {
503 wfRecursiveRemoveDir( $fileName );
504 }
505 }
506
507 if ( $this->needsDB() && $this->db ) {
508 // Clean up open transactions
509 while ( $this->db->trxLevel() > 0 ) {
510 $this->db->rollback( __METHOD__, 'flush' );
511 }
512 // Check for unsafe queries
513 if ( $this->db->getType() === 'mysql' ) {
514 $this->db->query( "SET sql_mode = 'STRICT_ALL_TABLES'" );
515 }
516 }
517
518 DeferredUpdates::clearPendingUpdates();
519 ObjectCache::getMainWANInstance()->clearProcessCache();
520
521 // XXX: reset maintenance triggers
522 // Hook into period lag checks which often happen in long-running scripts
523 $services = MediaWikiServices::getInstance();
524 $lbFactory = $services->getDBLoadBalancerFactory();
525 Maintenance::setLBFactoryTriggers( $lbFactory, $services->getMainConfig() );
526
527 ob_start( 'MediaWikiTestCase::wfResetOutputBuffersBarrier' );
528 }
529
530 protected function addTmpFiles( $files ) {
531 $this->tmpFiles = array_merge( $this->tmpFiles, (array)$files );
532 }
533
534 protected function tearDown() {
535 global $wgRequest, $wgSQLMode;
536
537 $status = ob_get_status();
538 if ( isset( $status['name'] ) &&
539 $status['name'] === 'MediaWikiTestCase::wfResetOutputBuffersBarrier'
540 ) {
541 ob_end_flush();
542 }
543
544 $this->called['tearDown'] = true;
545 // Cleaning up temporary files
546 foreach ( $this->tmpFiles as $fileName ) {
547 if ( is_file( $fileName ) || ( is_link( $fileName ) ) ) {
548 unlink( $fileName );
549 } elseif ( is_dir( $fileName ) ) {
550 wfRecursiveRemoveDir( $fileName );
551 }
552 }
553
554 if ( $this->needsDB() && $this->db ) {
555 // Clean up open transactions
556 while ( $this->db->trxLevel() > 0 ) {
557 $this->db->rollback( __METHOD__, 'flush' );
558 }
559 if ( $this->db->getType() === 'mysql' ) {
560 $this->db->query( "SET sql_mode = " . $this->db->addQuotes( $wgSQLMode ) );
561 }
562 }
563
564 // Restore mw globals
565 foreach ( $this->mwGlobals as $key => $value ) {
566 $GLOBALS[$key] = $value;
567 }
568 foreach ( $this->mwGlobalsToUnset as $value ) {
569 unset( $GLOBALS[$value] );
570 }
571 $this->mwGlobals = [];
572 $this->mwGlobalsToUnset = [];
573 $this->restoreLoggers();
574
575 if ( self::$serviceLocator && MediaWikiServices::getInstance() !== self::$serviceLocator ) {
576 MediaWikiServices::forceGlobalInstance( self::$serviceLocator );
577 }
578
579 // TODO: move global state into MediaWikiServices
581 if ( session_id() !== '' ) {
582 session_write_close();
583 session_id( '' );
584 }
585 $wgRequest = new FauxRequest();
586 MediaWiki\Session\SessionManager::resetCache();
587 MediaWiki\Auth\AuthManager::resetCache();
588
589 $phpErrorLevel = intval( ini_get( 'error_reporting' ) );
590
591 if ( $phpErrorLevel !== $this->phpErrorLevel ) {
592 ini_set( 'error_reporting', $this->phpErrorLevel );
593
594 $oldHex = strtoupper( dechex( $this->phpErrorLevel ) );
595 $newHex = strtoupper( dechex( $phpErrorLevel ) );
596 $message = "PHP error_reporting setting was left dirty: "
597 . "was 0x$oldHex before test, 0x$newHex after test!";
598
599 $this->fail( $message );
600 }
601
602 parent::tearDown();
603 }
604
614 $this->assertArrayHasKey( 'setUp', $this->called,
615 static::class . '::setUp() must call parent::setUp()'
616 );
617 }
618
628 protected function setService( $name, $object ) {
629 // If we did not yet override the service locator, so so now.
630 if ( MediaWikiServices::getInstance() === self::$serviceLocator ) {
631 $this->overrideMwServices();
632 }
633
634 MediaWikiServices::getInstance()->disableService( $name );
635 MediaWikiServices::getInstance()->redefineService(
636 $name,
637 function () use ( $object ) {
638 return $object;
639 }
640 );
641 }
642
678 protected function setMwGlobals( $pairs, $value = null ) {
679 if ( is_string( $pairs ) ) {
680 $pairs = [ $pairs => $value ];
681 }
682
683 $this->stashMwGlobals( array_keys( $pairs ) );
684
685 foreach ( $pairs as $key => $value ) {
686 $GLOBALS[$key] = $value;
687 }
688 }
689
698 private static function canShallowCopy( $value ) {
699 if ( is_scalar( $value ) || $value === null ) {
700 return true;
701 }
702 if ( is_array( $value ) ) {
703 foreach ( $value as $subValue ) {
704 if ( !is_scalar( $subValue ) && $subValue !== null ) {
705 return false;
706 }
707 }
708 return true;
709 }
710 return false;
711 }
712
730 protected function stashMwGlobals( $globalKeys ) {
731 if ( is_string( $globalKeys ) ) {
732 $globalKeys = [ $globalKeys ];
733 }
734
735 foreach ( $globalKeys as $globalKey ) {
736 // NOTE: make sure we only save the global once or a second call to
737 // setMwGlobals() on the same global would override the original
738 // value.
739 if (
740 !array_key_exists( $globalKey, $this->mwGlobals ) &&
741 !array_key_exists( $globalKey, $this->mwGlobalsToUnset )
742 ) {
743 if ( !array_key_exists( $globalKey, $GLOBALS ) ) {
744 $this->mwGlobalsToUnset[$globalKey] = $globalKey;
745 continue;
746 }
747 // NOTE: we serialize then unserialize the value in case it is an object
748 // this stops any objects being passed by reference. We could use clone
749 // and if is_object but this does account for objects within objects!
750 if ( self::canShallowCopy( $GLOBALS[$globalKey] ) ) {
751 $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
752 } elseif (
753 // Many MediaWiki types are safe to clone. These are the
754 // ones that are most commonly stashed.
755 $GLOBALS[$globalKey] instanceof Language ||
756 $GLOBALS[$globalKey] instanceof User ||
757 $GLOBALS[$globalKey] instanceof FauxRequest
758 ) {
759 $this->mwGlobals[$globalKey] = clone $GLOBALS[$globalKey];
760 } elseif ( $this->containsClosure( $GLOBALS[$globalKey] ) ) {
761 // Serializing Closure only gives a warning on HHVM while
762 // it throws an Exception on Zend.
763 // Workaround for https://github.com/facebook/hhvm/issues/6206
764 $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
765 } else {
766 try {
767 $this->mwGlobals[$globalKey] = unserialize( serialize( $GLOBALS[$globalKey] ) );
768 } catch ( Exception $e ) {
769 $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
770 }
771 }
772 }
773 }
774 }
775
782 private function containsClosure( $var, $maxDepth = 15 ) {
783 if ( $var instanceof Closure ) {
784 return true;
785 }
786 if ( !is_array( $var ) || $maxDepth === 0 ) {
787 return false;
788 }
789
790 foreach ( $var as $value ) {
791 if ( $this->containsClosure( $value, $maxDepth - 1 ) ) {
792 return true;
793 }
794 }
795 return false;
796 }
797
813 protected function mergeMwGlobalArrayValue( $name, $values ) {
814 if ( !isset( $GLOBALS[$name] ) ) {
815 $merged = $values;
816 } else {
817 if ( !is_array( $GLOBALS[$name] ) ) {
818 throw new MWException( "MW global $name is not an array." );
819 }
820
821 // NOTE: do not use array_merge, it screws up for numeric keys.
822 $merged = $GLOBALS[$name];
823 foreach ( $values as $k => $v ) {
824 $merged[$k] = $v;
825 }
826 }
827
828 $this->setMwGlobals( $name, $merged );
829 }
830
845 protected function overrideMwServices( Config $configOverrides = null, array $services = [] ) {
846 if ( !$configOverrides ) {
847 $configOverrides = new HashConfig();
848 }
849
850 $oldInstance = MediaWikiServices::getInstance();
851 $oldConfigFactory = $oldInstance->getConfigFactory();
852 $oldLoadBalancerFactory = $oldInstance->getDBLoadBalancerFactory();
853
854 $testConfig = self::makeTestConfig( null, $configOverrides );
855 $newInstance = new MediaWikiServices( $testConfig );
856
857 // Load the default wiring from the specified files.
858 // NOTE: this logic mirrors the logic in MediaWikiServices::newInstance.
859 $wiringFiles = $testConfig->get( 'ServiceWiringFiles' );
860 $newInstance->loadWiringFiles( $wiringFiles );
861
862 // Provide a traditional hook point to allow extensions to configure services.
863 Hooks::run( 'MediaWikiServices', [ $newInstance ] );
864
865 foreach ( $services as $name => $callback ) {
866 $newInstance->redefineService( $name, $callback );
867 }
868
869 self::installTestServices(
870 $oldConfigFactory,
871 $oldLoadBalancerFactory,
872 $newInstance
873 );
874 MediaWikiServices::forceGlobalInstance( $newInstance );
875
876 return $newInstance;
877 }
878
883 public function setUserLang( $lang ) {
884 RequestContext::getMain()->setLanguage( $lang );
885 $this->setMwGlobals( 'wgLang', RequestContext::getMain()->getLanguage() );
886 }
887
892 public function setContentLang( $lang ) {
893 if ( $lang instanceof Language ) {
894 $langCode = $lang->getCode();
895 $langObj = $lang;
896 } else {
897 $langCode = $lang;
898 $langObj = Language::factory( $langCode );
899 }
900 $this->setMwGlobals( [
901 'wgLanguageCode' => $langCode,
902 'wgContLang' => $langObj,
903 ] );
904 }
905
920 public function setGroupPermissions( $newPerms, $newKey = null, $newValue = null ) {
921 global $wgGroupPermissions;
922
923 $this->stashMwGlobals( 'wgGroupPermissions' );
924
925 if ( is_string( $newPerms ) ) {
926 $newPerms = [ $newPerms => [ $newKey => $newValue ] ];
927 }
928
929 foreach ( $newPerms as $group => $permissions ) {
930 foreach ( $permissions as $key => $value ) {
931 $wgGroupPermissions[$group][$key] = $value;
932 }
933 }
934 }
935
942 protected function setLogger( $channel, LoggerInterface $logger ) {
943 // TODO: Once loggers are managed by MediaWikiServices, use
944 // overrideMwServices() to set loggers.
945
946 $provider = LoggerFactory::getProvider();
947 $wrappedProvider = TestingAccessWrapper::newFromObject( $provider );
948 $singletons = $wrappedProvider->singletons;
949 if ( $provider instanceof MonologSpi ) {
950 if ( !isset( $this->loggers[$channel] ) ) {
951 $this->loggers[$channel] = isset( $singletons['loggers'][$channel] )
952 ? $singletons['loggers'][$channel] : null;
953 }
954 $singletons['loggers'][$channel] = $logger;
955 } elseif ( $provider instanceof LegacySpi ) {
956 if ( !isset( $this->loggers[$channel] ) ) {
957 $this->loggers[$channel] = isset( $singletons[$channel] ) ? $singletons[$channel] : null;
958 }
959 $singletons[$channel] = $logger;
960 } else {
961 throw new LogicException( __METHOD__ . ': setting a logger for ' . get_class( $provider )
962 . ' is not implemented' );
963 }
964 $wrappedProvider->singletons = $singletons;
965 }
966
971 private function restoreLoggers() {
972 $provider = LoggerFactory::getProvider();
973 $wrappedProvider = TestingAccessWrapper::newFromObject( $provider );
974 $singletons = $wrappedProvider->singletons;
975 foreach ( $this->loggers as $channel => $logger ) {
976 if ( $provider instanceof MonologSpi ) {
977 if ( $logger === null ) {
978 unset( $singletons['loggers'][$channel] );
979 } else {
980 $singletons['loggers'][$channel] = $logger;
981 }
982 } elseif ( $provider instanceof LegacySpi ) {
983 if ( $logger === null ) {
984 unset( $singletons[$channel] );
985 } else {
986 $singletons[$channel] = $logger;
987 }
988 }
989 }
990 $wrappedProvider->singletons = $singletons;
991 $this->loggers = [];
992 }
993
998 public function dbPrefix() {
999 return $this->db->getType() == 'oracle' ? self::ORA_DB_PREFIX : self::DB_PREFIX;
1000 }
1001
1006 public function needsDB() {
1007 // If the test says it uses database tables, it needs the database
1008 if ( $this->tablesUsed ) {
1009 return true;
1010 }
1011
1012 // If the test class says it belongs to the Database group, it needs the database.
1013 // NOTE: This ONLY checks for the group in the class level doc comment.
1014 $rc = new ReflectionClass( $this );
1015 if ( preg_match( '/@group +Database/im', $rc->getDocComment() ) ) {
1016 return true;
1017 }
1018
1019 return false;
1020 }
1021
1033 protected function insertPage(
1034 $pageName,
1035 $text = 'Sample page for unit test.',
1036 $namespace = null
1037 ) {
1038 if ( is_string( $pageName ) ) {
1039 $title = Title::newFromText( $pageName, $namespace );
1040 } else {
1041 $title = $pageName;
1042 }
1043
1044 $user = static::getTestSysop()->getUser();
1045 $comment = __METHOD__ . ': Sample page for unit test.';
1046
1047 $page = WikiPage::factory( $title );
1048 $page->doEditContent( ContentHandler::makeContent( $text, $title ), $comment, 0, false, $user );
1049
1050 return [
1051 'title' => $title,
1052 'id' => $page->getId(),
1053 ];
1054 }
1055
1071 public function addDBDataOnce() {
1072 }
1073
1083 public function addDBData() {
1084 }
1085
1086 private function addCoreDBData() {
1087 if ( $this->db->getType() == 'oracle' ) {
1088 # Insert 0 user to prevent FK violations
1089 # Anonymous user
1090 if ( !$this->db->selectField( 'user', '1', [ 'user_id' => 0 ] ) ) {
1091 $this->db->insert( 'user', [
1092 'user_id' => 0,
1093 'user_name' => 'Anonymous' ], __METHOD__, [ 'IGNORE' ] );
1094 }
1095
1096 # Insert 0 page to prevent FK violations
1097 # Blank page
1098 if ( !$this->db->selectField( 'page', '1', [ 'page_id' => 0 ] ) ) {
1099 $this->db->insert( 'page', [
1100 'page_id' => 0,
1101 'page_namespace' => 0,
1102 'page_title' => ' ',
1103 'page_restrictions' => null,
1104 'page_is_redirect' => 0,
1105 'page_is_new' => 0,
1106 'page_random' => 0,
1107 'page_touched' => $this->db->timestamp(),
1108 'page_latest' => 0,
1109 'page_len' => 0 ], __METHOD__, [ 'IGNORE' ] );
1110 }
1111 }
1112
1114
1116
1117 // Make sysop user
1118 $user = static::getTestSysop()->getUser();
1119
1120 // Make 1 page with 1 revision
1121 $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
1122 if ( $page->getId() == 0 ) {
1123 $page->doEditContent(
1124 new WikitextContent( 'UTContent' ),
1125 'UTPageSummary',
1127 false,
1128 $user
1129 );
1130 // an edit always attempt to purge backlink links such as history
1131 // pages. That is unneccessary.
1132 JobQueueGroup::singleton()->get( 'htmlCacheUpdate' )->delete();
1133 // WikiPages::doEditUpdates randomly adds RC purges
1134 JobQueueGroup::singleton()->get( 'recentChangesUpdate' )->delete();
1135
1136 // doEditContent() probably started the session via
1137 // User::loadFromSession(). Close it now.
1138 if ( session_id() !== '' ) {
1139 session_write_close();
1140 session_id( '' );
1141 }
1142 }
1143 }
1144
1152 public static function teardownTestDB() {
1153 global $wgJobClasses;
1154
1155 if ( !self::$dbSetup ) {
1156 return;
1157 }
1158
1159 Hooks::run( 'UnitTestsBeforeDatabaseTeardown' );
1160
1161 foreach ( $wgJobClasses as $type => $class ) {
1162 // Delete any jobs under the clone DB (or old prefix in other stores)
1163 JobQueueGroup::singleton()->get( $type )->delete();
1164 }
1165
1166 CloneDatabase::changePrefix( self::$oldTablePrefix );
1167
1168 self::$oldTablePrefix = false;
1169 self::$dbSetup = false;
1170 }
1171
1185 protected static function setupDatabaseWithTestPrefix( IMaintainableDatabase $db, $prefix ) {
1186 $tablesCloned = self::listTables( $db );
1187 $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix );
1188 $dbClone->useTemporaryTables( self::$useTemporaryTables );
1189
1190 $db->_originalTablePrefix = $db->tablePrefix();
1191
1192 if ( ( $db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
1193 CloneDatabase::changePrefix( $prefix );
1194
1195 return false;
1196 } else {
1197 $dbClone->cloneTableStructure();
1198 return true;
1199 }
1200 }
1201
1205 public function setupAllTestDBs() {
1206 global $wgDBprefix;
1207
1208 self::$oldTablePrefix = $wgDBprefix;
1209
1210 $testPrefix = $this->dbPrefix();
1211
1212 // switch to a temporary clone of the database
1213 self::setupTestDB( $this->db, $testPrefix );
1214
1215 if ( self::isUsingExternalStoreDB() ) {
1216 self::setupExternalStoreTestDBs( $testPrefix );
1217 }
1218 }
1219
1241 public static function setupTestDB( Database $db, $prefix ) {
1242 if ( self::$dbSetup ) {
1243 return;
1244 }
1245
1246 if ( $db->tablePrefix() === $prefix ) {
1247 throw new MWException(
1248 'Cannot run unit tests, the database prefix is already "' . $prefix . '"' );
1249 }
1250
1251 // TODO: the below should be re-written as soon as LBFactory, LoadBalancer,
1252 // and Database no longer use global state.
1253
1254 self::$dbSetup = true;
1255
1256 if ( !self::setupDatabaseWithTestPrefix( $db, $prefix ) ) {
1257 return;
1258 }
1259
1260 // Assuming this isn't needed for External Store database, and not sure if the procedure
1261 // would be available there.
1262 if ( $db->getType() == 'oracle' ) {
1263 $db->query( 'BEGIN FILL_WIKI_INFO; END;' );
1264 }
1265
1266 Hooks::run( 'UnitTestsAfterDatabaseSetup', [ $db, $prefix ] );
1267 }
1268
1274 protected static function setupExternalStoreTestDBs( $testPrefix ) {
1275 $connections = self::getExternalStoreDatabaseConnections();
1276 foreach ( $connections as $dbw ) {
1277 // Hack: cloneTableStructure sets $wgDBprefix to the unit test
1278 // prefix,. Even though listTables now uses tablePrefix, that
1279 // itself is populated from $wgDBprefix by default.
1280
1281 // We have to set it back, or we won't find the original 'blobs'
1282 // table to copy.
1283
1284 $dbw->tablePrefix( self::$oldTablePrefix );
1285 self::setupDatabaseWithTestPrefix( $dbw, $testPrefix );
1286 }
1287 }
1288
1295 protected static function getExternalStoreDatabaseConnections() {
1297
1299 $externalStoreDB = ExternalStore::getStoreObject( 'DB' );
1300 $defaultArray = (array)$wgDefaultExternalStore;
1301 $dbws = [];
1302 foreach ( $defaultArray as $url ) {
1303 if ( strpos( $url, 'DB://' ) === 0 ) {
1304 list( $proto, $cluster ) = explode( '://', $url, 2 );
1305 // Avoid getMaster() because setupDatabaseWithTestPrefix()
1306 // requires Database instead of plain DBConnRef/IDatabase
1307 $dbws[] = $externalStoreDB->getMaster( $cluster );
1308 }
1309 }
1310
1311 return $dbws;
1312 }
1313
1319 protected static function isUsingExternalStoreDB() {
1321 if ( !$wgDefaultExternalStore ) {
1322 return false;
1323 }
1324
1325 $defaultArray = (array)$wgDefaultExternalStore;
1326 foreach ( $defaultArray as $url ) {
1327 if ( strpos( $url, 'DB://' ) === 0 ) {
1328 return true;
1329 }
1330 }
1331
1332 return false;
1333 }
1334
1339 private function ensureMockDatabaseConnection( IDatabase $db ) {
1340 if ( $db->tablePrefix() !== $this->dbPrefix() ) {
1341 throw new LogicException(
1342 'Trying to delete mock tables, but table prefix does not indicate a mock database.'
1343 );
1344 }
1345 }
1346
1347 private static $schemaOverrideDefaults = [
1348 'scripts' => [],
1349 'create' => [],
1350 'drop' => [],
1351 'alter' => [],
1352 ];
1353
1368 protected function getSchemaOverrides( IMaintainableDatabase $db ) {
1369 return [];
1370 }
1371
1379 private function undoSchemaOverrides( IMaintainableDatabase $db, $oldOverrides ) {
1380 $this->ensureMockDatabaseConnection( $db );
1381
1382 $oldOverrides = $oldOverrides + self::$schemaOverrideDefaults;
1383 $originalTables = $this->listOriginalTables( $db );
1384
1385 // Drop tables that need to be restored or removed.
1386 $tablesToDrop = array_merge( $oldOverrides['create'], $oldOverrides['alter'] );
1387
1388 // Restore tables that have been dropped or created or altered,
1389 // if they exist in the original schema.
1390 $tablesToRestore = array_merge( $tablesToDrop, $oldOverrides['drop'] );
1391 $tablesToRestore = array_intersect( $originalTables, $tablesToRestore );
1392
1393 if ( $tablesToDrop ) {
1394 $this->dropMockTables( $db, $tablesToDrop );
1395 }
1396
1397 if ( $tablesToRestore ) {
1398 $this->recloneMockTables( $db, $tablesToRestore );
1399 }
1400 }
1401
1407 private function setUpSchema( IMaintainableDatabase $db ) {
1408 // Undo any active overrides.
1409 $oldOverrides = isset( $db->_schemaOverrides ) ? $db->_schemaOverrides
1410 : self::$schemaOverrideDefaults;
1411
1412 if ( $oldOverrides['alter'] || $oldOverrides['create'] || $oldOverrides['drop'] ) {
1413 $this->undoSchemaOverrides( $db, $oldOverrides );
1414 }
1415
1416 // Determine new overrides.
1417 $overrides = $this->getSchemaOverrides( $db ) + self::$schemaOverrideDefaults;
1418
1419 $extraKeys = array_diff(
1420 array_keys( $overrides ),
1421 array_keys( self::$schemaOverrideDefaults )
1422 );
1423
1424 if ( $extraKeys ) {
1425 throw new InvalidArgumentException(
1426 'Schema override contains extra keys: ' . var_export( $extraKeys, true )
1427 );
1428 }
1429
1430 if ( !$overrides['scripts'] ) {
1431 // no scripts to run
1432 return;
1433 }
1434
1435 if ( !$overrides['create'] && !$overrides['drop'] && !$overrides['alter'] ) {
1436 throw new InvalidArgumentException(
1437 'Schema override scripts given, but no tables are declared to be '
1438 . 'created, dropped or altered.'
1439 );
1440 }
1441
1442 $this->ensureMockDatabaseConnection( $db );
1443
1444 // Drop the tables that will be created by the schema scripts.
1445 $originalTables = $this->listOriginalTables( $db );
1446 $tablesToDrop = array_intersect( $originalTables, $overrides['create'] );
1447
1448 if ( $tablesToDrop ) {
1449 $this->dropMockTables( $db, $tablesToDrop );
1450 }
1451
1452 // Run schema override scripts.
1453 foreach ( $overrides['scripts'] as $script ) {
1454 $db->sourceFile(
1455 $script,
1456 null,
1457 null,
1458 __METHOD__,
1459 function ( $cmd ) {
1460 return $this->mungeSchemaUpdateQuery( $cmd );
1461 }
1462 );
1463 }
1464
1465 $db->_schemaOverrides = $overrides;
1466 }
1467
1468 private function mungeSchemaUpdateQuery( $cmd ) {
1469 return self::$useTemporaryTables
1470 ? preg_replace( '/\bCREATE\s+TABLE\b/i', 'CREATE TEMPORARY TABLE', $cmd )
1471 : $cmd;
1472 }
1473
1480 private function dropMockTables( IMaintainableDatabase $db, array $tables ) {
1481 $this->ensureMockDatabaseConnection( $db );
1482
1483 foreach ( $tables as $tbl ) {
1484 $tbl = $db->tableName( $tbl );
1485 $db->query( "DROP TABLE IF EXISTS $tbl", __METHOD__ );
1486
1487 if ( $tbl === 'page' ) {
1488 // Forget about the pages since they don't
1489 // exist in the DB.
1490 LinkCache::singleton()->clear();
1491 }
1492 }
1493 }
1494
1502 if ( !isset( $db->_originalTablePrefix ) ) {
1503 throw new LogicException( 'No original table prefix know, cannot list tables!' );
1504 }
1505
1506 $originalTables = $db->listTables( $db->_originalTablePrefix, __METHOD__ );
1507 return $originalTables;
1508 }
1509
1518 private function recloneMockTables( IMaintainableDatabase $db, array $tables ) {
1519 $this->ensureMockDatabaseConnection( $db );
1520
1521 if ( !isset( $db->_originalTablePrefix ) ) {
1522 throw new LogicException( 'No original table prefix know, cannot restore tables!' );
1523 }
1524
1525 $originalTables = $this->listOriginalTables( $db );
1526 $tables = array_intersect( $tables, $originalTables );
1527
1528 $dbClone = new CloneDatabase( $db, $tables, $db->tablePrefix(), $db->_originalTablePrefix );
1529 $dbClone->useTemporaryTables( self::$useTemporaryTables );
1530
1531 $dbClone->cloneTableStructure();
1532 }
1533
1540 private function resetDB( $db, $tablesUsed ) {
1541 if ( $db ) {
1542 $userTables = [ 'user', 'user_groups', 'user_properties', 'actor' ];
1543 $pageTables = [ 'page', 'revision', 'ip_changes', 'revision_comment_temp',
1544 'revision_actor_temp', 'comment' ];
1545 $coreDBDataTables = array_merge( $userTables, $pageTables );
1546
1547 // If any of the user or page tables were marked as used, we should clear all of them.
1548 if ( array_intersect( $tablesUsed, $userTables ) ) {
1549 $tablesUsed = array_unique( array_merge( $tablesUsed, $userTables ) );
1550 TestUserRegistry::clear();
1551 }
1552 if ( array_intersect( $tablesUsed, $pageTables ) ) {
1553 $tablesUsed = array_unique( array_merge( $tablesUsed, $pageTables ) );
1554 }
1555
1556 $truncate = in_array( $db->getType(), [ 'oracle', 'mysql' ] );
1557 foreach ( $tablesUsed as $tbl ) {
1558 // TODO: reset interwiki table to its original content.
1559 if ( $tbl == 'interwiki' ) {
1560 continue;
1561 }
1562
1563 if ( !$db->tableExists( $tbl ) ) {
1564 continue;
1565 }
1566
1567 if ( $truncate ) {
1568 $db->query( 'TRUNCATE TABLE ' . $db->tableName( $tbl ), __METHOD__ );
1569 } else {
1570 $db->delete( $tbl, '*', __METHOD__ );
1571 }
1572
1573 if ( in_array( $db->getType(), [ 'postgres', 'sqlite' ], true ) ) {
1574 // Reset the table's sequence too.
1575 $db->resetSequenceForTable( $tbl, __METHOD__ );
1576 }
1577
1578 if ( $tbl === 'page' ) {
1579 // Forget about the pages since they don't
1580 // exist in the DB.
1581 LinkCache::singleton()->clear();
1582 }
1583 }
1584
1585 if ( array_intersect( $tablesUsed, $coreDBDataTables ) ) {
1586 // Re-add core DB data that was deleted
1587 $this->addCoreDBData();
1588 }
1589 }
1590 }
1591
1592 private static function unprefixTable( &$tableName, $ind, $prefix ) {
1593 $tableName = substr( $tableName, strlen( $prefix ) );
1594 }
1595
1596 private static function isNotUnittest( $table ) {
1597 return strpos( $table, self::DB_PREFIX ) !== 0;
1598 }
1599
1607 public static function listTables( IMaintainableDatabase $db ) {
1608 $prefix = $db->tablePrefix();
1609 $tables = $db->listTables( $prefix, __METHOD__ );
1610
1611 if ( $db->getType() === 'mysql' ) {
1612 static $viewListCache = null;
1613 if ( $viewListCache === null ) {
1614 $viewListCache = $db->listViews( null, __METHOD__ );
1615 }
1616 // T45571: cannot clone VIEWs under MySQL
1617 $tables = array_diff( $tables, $viewListCache );
1618 }
1619 array_walk( $tables, [ __CLASS__, 'unprefixTable' ], $prefix );
1620
1621 // Don't duplicate test tables from the previous fataled run
1622 $tables = array_filter( $tables, [ __CLASS__, 'isNotUnittest' ] );
1623
1624 if ( $db->getType() == 'sqlite' ) {
1625 $tables = array_flip( $tables );
1626 // these are subtables of searchindex and don't need to be duped/dropped separately
1627 unset( $tables['searchindex_content'] );
1628 unset( $tables['searchindex_segdir'] );
1629 unset( $tables['searchindex_segments'] );
1630 $tables = array_flip( $tables );
1631 }
1632
1633 return $tables;
1634 }
1635
1640 protected function checkDbIsSupported() {
1641 if ( !in_array( $this->db->getType(), $this->supportedDBs ) ) {
1642 throw new MWException( $this->db->getType() . " is not currently supported for unit testing." );
1643 }
1644 }
1645
1651 public function getCliArg( $offset ) {
1652 if ( isset( PHPUnitMaintClass::$additionalOptions[$offset] ) ) {
1654 }
1655
1656 return null;
1657 }
1658
1664 public function setCliArg( $offset, $value ) {
1666 }
1667
1675 public function hideDeprecated( $function ) {
1676 Wikimedia\suppressWarnings();
1677 wfDeprecated( $function );
1678 Wikimedia\restoreWarnings();
1679 }
1680
1701 protected function assertSelect(
1702 $table, $fields, $condition, array $expectedRows, array $options = [], array $join_conds = []
1703 ) {
1704 if ( !$this->needsDB() ) {
1705 throw new MWException( 'When testing database state, the test cases\'s needDB()' .
1706 ' method should return true. Use @group Database or $this->tablesUsed.' );
1707 }
1708
1709 $db = wfGetDB( DB_REPLICA );
1710
1711 $res = $db->select(
1712 $table,
1713 $fields,
1714 $condition,
1715 wfGetCaller(),
1716 $options + [ 'ORDER BY' => $fields ],
1717 $join_conds
1718 );
1719 $this->assertNotEmpty( $res, "query failed: " . $db->lastError() );
1720
1721 $i = 0;
1722
1723 foreach ( $expectedRows as $expected ) {
1724 $r = $res->fetchRow();
1725 self::stripStringKeys( $r );
1726
1727 $i += 1;
1728 $this->assertNotEmpty( $r, "row #$i missing" );
1729
1730 $this->assertEquals( $expected, $r, "row #$i mismatches" );
1731 }
1732
1733 $r = $res->fetchRow();
1734 self::stripStringKeys( $r );
1735
1736 $this->assertFalse( $r, "found extra row (after #$i)" );
1737 }
1738
1750 protected function arrayWrap( array $elements ) {
1751 return array_map(
1752 function ( $element ) {
1753 return [ $element ];
1754 },
1755 $elements
1756 );
1757 }
1758
1771 protected function assertArrayEquals( array $expected, array $actual,
1772 $ordered = false, $named = false
1773 ) {
1774 if ( !$ordered ) {
1775 $this->objectAssociativeSort( $expected );
1776 $this->objectAssociativeSort( $actual );
1777 }
1778
1779 if ( !$named ) {
1780 $expected = array_values( $expected );
1781 $actual = array_values( $actual );
1782 }
1783
1784 call_user_func_array(
1785 [ $this, 'assertEquals' ],
1786 array_merge( [ $expected, $actual ], array_slice( func_get_args(), 4 ) )
1787 );
1788 }
1789
1802 protected function assertHTMLEquals( $expected, $actual, $msg = '' ) {
1803 $expected = str_replace( '>', ">\n", $expected );
1804 $actual = str_replace( '>', ">\n", $actual );
1805
1806 $this->assertEquals( $expected, $actual, $msg );
1807 }
1808
1816 protected function objectAssociativeSort( array &$array ) {
1817 uasort(
1818 $array,
1819 function ( $a, $b ) {
1820 return serialize( $a ) > serialize( $b ) ? 1 : -1;
1821 }
1822 );
1823 }
1824
1834 protected static function stripStringKeys( &$r ) {
1835 if ( !is_array( $r ) ) {
1836 return;
1837 }
1838
1839 foreach ( $r as $k => $v ) {
1840 if ( is_string( $k ) ) {
1841 unset( $r[$k] );
1842 }
1843 }
1844 }
1845
1859 protected function assertTypeOrValue( $type, $actual, $value = false, $message = '' ) {
1860 if ( $actual === $value ) {
1861 $this->assertTrue( true, $message );
1862 } else {
1863 $this->assertType( $type, $actual, $message );
1864 }
1865 }
1866
1878 protected function assertType( $type, $actual, $message = '' ) {
1879 if ( class_exists( $type ) || interface_exists( $type ) ) {
1880 $this->assertInstanceOf( $type, $actual, $message );
1881 } else {
1882 $this->assertInternalType( $type, $actual, $message );
1883 }
1884 }
1885
1895 protected function isWikitextNS( $ns ) {
1897
1898 if ( isset( $wgNamespaceContentModels[$ns] ) ) {
1899 return $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT;
1900 }
1901
1902 return true;
1903 }
1904
1912 protected function getDefaultWikitextNS() {
1914
1915 static $wikitextNS = null; // this is not going to change
1916 if ( $wikitextNS !== null ) {
1917 return $wikitextNS;
1918 }
1919
1920 // quickly short out on most common case:
1921 if ( !isset( $wgNamespaceContentModels[NS_MAIN] ) ) {
1922 return NS_MAIN;
1923 }
1924
1925 // NOTE: prefer content namespaces
1926 $namespaces = array_unique( array_merge(
1927 MWNamespace::getContentNamespaces(),
1928 [ NS_MAIN, NS_HELP, NS_PROJECT ], // prefer these
1929 MWNamespace::getValidNamespaces()
1930 ) );
1931
1932 $namespaces = array_diff( $namespaces, [
1933 NS_FILE, NS_CATEGORY, NS_MEDIAWIKI, NS_USER // don't mess with magic namespaces
1934 ] );
1935
1936 $talk = array_filter( $namespaces, function ( $ns ) {
1937 return MWNamespace::isTalk( $ns );
1938 } );
1939
1940 // prefer non-talk pages
1941 $namespaces = array_diff( $namespaces, $talk );
1942 $namespaces = array_merge( $namespaces, $talk );
1943
1944 // check default content model of each namespace
1945 foreach ( $namespaces as $ns ) {
1946 if ( !isset( $wgNamespaceContentModels[$ns] ) ||
1947 $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT
1948 ) {
1949 $wikitextNS = $ns;
1950
1951 return $wikitextNS;
1952 }
1953 }
1954
1955 // give up
1956 // @todo Inside a test, we could skip the test as incomplete.
1957 // But frequently, this is used in fixture setup.
1958 throw new MWException( "No namespace defaults to wikitext!" );
1959 }
1960
1967 protected function markTestSkippedIfNoDiff3() {
1968 global $wgDiff3;
1969
1970 # This check may also protect against code injection in
1971 # case of broken installations.
1972 Wikimedia\suppressWarnings();
1973 $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 );
1974 Wikimedia\restoreWarnings();
1975
1976 if ( !$haveDiff3 ) {
1977 $this->markTestSkipped( "Skip test, since diff3 is not configured" );
1978 }
1979 }
1980
1989 protected function checkPHPExtension( $extName ) {
1990 $loaded = extension_loaded( $extName );
1991 if ( !$loaded ) {
1992 $this->markTestSkipped( "PHP extension '$extName' is not loaded, skipping." );
1993 }
1994
1995 return $loaded;
1996 }
1997
2003 public static function wfResetOutputBuffersBarrier( $buffer ) {
2004 return $buffer;
2005 }
2006
2014 protected function setTemporaryHook( $hookName, $handler ) {
2015 $this->mergeMwGlobalArrayValue( 'wgHooks', [ $hookName => [ $handler ] ] );
2016 }
2017
2027 protected function assertFileContains(
2028 $fileName,
2029 $actualData,
2030 $createIfMissing = true,
2031 $msg = ''
2032 ) {
2033 if ( $createIfMissing ) {
2034 if ( !file_exists( $fileName ) ) {
2035 file_put_contents( $fileName, $actualData );
2036 $this->markTestSkipped( 'Data file $fileName does not exist' );
2037 }
2038 } else {
2039 self::assertFileExists( $fileName );
2040 }
2041 self::assertEquals( file_get_contents( $fileName ), $actualData, $msg );
2042 }
2043}
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.
$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.
$wgGroupPermissions['sysop']['replacetext']
$wgJobClasses['replaceText']
if(! $wgDBerrorLogTZ) $wgRequest
Definition Setup.php:737
static changePrefix( $prefix)
Change the table prefix on all open DB connections/.
Factory class to create Config objects.
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 singleton( $domain=false)
static destroySingletons()
Destroy the singleton instances.
Internationalisation code.
Definition Language.php:35
MediaWiki exception.
static setLBFactoryTriggers(LBFactory $LBFactory, Config $config)
dropMockTables(IMaintainableDatabase $db, array $tables)
Drops the given mock tables.
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
listOriginalTables(IMaintainableDatabase $db)
Lists all tables in the live database schema.
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.
recloneMockTables(IMaintainableDatabase $db, array $tables)
Re-clones the given mock tables to restore them based on the live database schema.
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.
setGroupPermissions( $newPerms, $newKey=null, $newValue=null)
Alters $wgGroupPermissions for the duration of the test.
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().
getSchemaOverrides(IMaintainableDatabase $db)
Stub.
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.
setUpSchema(IMaintainableDatabase $db)
Applies the schema overrides returned by getSchemaOverrides(), after undoing any previously applied s...
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...
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.
ensureMockDatabaseConnection(IDatabase $db)
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.
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.
assertSelect( $table, $fields, $condition, array $expectedRows, array $options=[], array $join_conds=[])
Asserts that the given database query yields the rows given by $expectedRows.
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)
undoSchemaOverrides(IMaintainableDatabase $db, $oldOverrides)
Undoes the dpecified schema overrides.
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.
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.
Provides a fallback sequence for Config objects.
static $additionalOptions
Definition phpunit.php:18
static resetMain()
Resets singleton returned by getMain().
static getMain()
Get the RequestContext object associated with the main request.
static doPlaceholderInit()
Insert a dummy row with all zeroes if no row is present.
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:53
static resetIdByNameCache()
Reset the cache used in idFromName().
Definition User.php:923
Relational database abstraction object.
Definition Database.php:48
tablePrefix( $prefix=null)
Get/set the table prefix.
Definition Database.php:593
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.
tableExists( $table, $fname=__METHOD__)
Query whether a given table exists.
query( $sql, $fname=__METHOD__, $tempIgnore=false)
Run an SQL query and return the result.
An interface for generating database load balancers.
Definition LBFactory.php:39
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
the array() calling protocol came about after MediaWiki 1.4rc1.
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). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. '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:1051
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:1015
namespace and then decline to actually register it & $namespaces
Definition hooks.txt:934
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 & $options
Definition hooks.txt:2001
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:964
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:2273
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:903
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
returning false will NOT prevent logging $e
Definition hooks.txt:2176
const NS_HELP
Definition Defines.php:86
const NS_FILE
Definition Defines.php:80
const CACHE_NONE
Definition Defines.php:112
const NS_MAIN
Definition Defines.php:74
const CACHE_ACCEL
Definition Defines.php:115
const EDIT_SUPPRESS_RC
Definition Defines.php:165
const CONTENT_MODEL_WIKITEXT
Definition Defines.php:245
const CACHE_MEMCACHED
Definition Defines.php:114
const CACHE_DB
Definition Defines.php:113
const NS_PROJECT
Definition Defines.php:78
const NS_CATEGORY
Definition Defines.php:88
const EDIT_NEW
Definition Defines.php:162
Interface for configuration instances.
Definition Config.php:28
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
query( $sql, $fname=__METHOD__, $tempIgnore=false)
Run an SQL query and return the result.
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.
tableName( $name, $format='quoted')
Format a table name ready for use in constructing an SQL query.
listViews( $prefix=null, $fname=__METHOD__)
Lists all the VIEWs in the database.
listTables( $prefix=null, $fname=__METHOD__)
List all tables on the database.
sourceFile( $filename, callable $lineCallback=null, callable $resultCallback=null, $fname=false, callable $inputCallback=null)
Read and execute SQL commands from a file.
$buffer
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:29
if(!isset( $args[0])) $lang