MediaWiki  master
MediaWikiTestCase.php
Go to the documentation of this file.
1 <?php
2 
12 
16 abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase {
17 
20 
26  private static $originalServices;
27 
32  private $localServices;
33 
46  private $called = [];
47 
52  public static $users;
53 
60  protected $db;
61 
66  protected $tablesUsed = []; // tables with data
67 
68  private static $useTemporaryTables = true;
69  private static $reuseDB = false;
70  private static $dbSetup = false;
71  private static $oldTablePrefix = '';
72 
78  private $phpErrorLevel;
79 
86  private $tmpFiles = [];
87 
94  private $mwGlobals = [];
95 
101  private $mwGlobalsToUnset = [];
102 
109  private $iniSettings = [];
110 
115  private $loggers = [];
116 
121  private $cliArgs = [];
122 
128  private $overriddenServices = [];
129 
133  const DB_PREFIX = 'unittest_';
134  const ORA_DB_PREFIX = 'ut_';
135 
140  protected $supportedDBs = [
141  'mysql',
142  'sqlite',
143  'postgres',
144  'oracle'
145  ];
146 
147  public function __construct( $name = null, array $data = [], $dataName = '' ) {
148  parent::__construct( $name, $data, $dataName );
149 
150  $this->backupGlobals = false;
151  $this->backupStaticAttributes = false;
152  }
153 
154  public function __destruct() {
155  // Complain if self::setUp() was called, but not self::tearDown()
156  // $this->called['setUp'] will be checked by self::testMediaWikiTestCaseParentSetupCalled()
157  if ( isset( $this->called['setUp'] ) && !isset( $this->called['tearDown'] ) ) {
158  throw new MWException( static::class . "::tearDown() must call parent::tearDown()" );
159  }
160  }
161 
162  public static function setUpBeforeClass() {
163  parent::setUpBeforeClass();
164 
165  // Get the original service locator
166  if ( !self::$originalServices ) {
167  self::$originalServices = MediaWikiServices::getInstance();
168  }
169  }
170 
179  public static function getTestUser( $groups = [] ) {
180  return TestUserRegistry::getImmutableTestUser( $groups );
181  }
182 
191  public static function getMutableTestUser( $groups = [] ) {
192  return TestUserRegistry::getMutableTestUser( __CLASS__, $groups );
193  }
194 
203  public static function getTestSysop() {
204  return self::getTestUser( [ 'sysop', 'bureaucrat' ] );
205  }
206 
219  protected function getExistingTestPage( $title = null ) {
220  if ( !$this->needsDB() ) {
221  throw new MWException( 'When testing which pages, the test cases\'s needsDB()' .
222  ' method should return true. Use @group Database or $this->tablesUsed.' );
223  }
224 
225  $title = ( $title === null ) ? 'UTPage' : $title;
226  $title = is_string( $title ) ? Title::newFromText( $title ) : $title;
227  $page = WikiPage::factory( $title );
228 
229  if ( !$page->exists() ) {
230  $user = self::getTestSysop()->getUser();
231  $page->doEditContent(
232  new WikitextContent( 'UTContent' ),
233  'UTPageSummary',
235  false,
236  $user
237  );
238  }
239 
240  return $page;
241  }
242 
255  protected function getNonexistingTestPage( $title = null ) {
256  if ( !$this->needsDB() ) {
257  throw new MWException( 'When testing which pages, the test cases\'s needsDB()' .
258  ' method should return true. Use @group Database or $this->tablesUsed.' );
259  }
260 
261  $title = ( $title === null ) ? 'UTPage-' . rand( 0, 100000 ) : $title;
262  $title = is_string( $title ) ? Title::newFromText( $title ) : $title;
263  $page = WikiPage::factory( $title );
264 
265  if ( $page->exists() ) {
266  $page->doDeleteArticle( 'Testing' );
267  }
268 
269  return $page;
270  }
271 
275  public static function prepareServices( Config $bootstrapConfig ) {
276  }
277 
287  private static function makeTestConfig(
288  Config $baseConfig = null,
289  Config $customOverrides = null
290  ) {
291  $defaultOverrides = new HashConfig();
292 
293  if ( !$baseConfig ) {
294  $baseConfig = self::$originalServices->getBootstrapConfig();
295  }
296 
297  /* Some functions require some kind of caching, and will end up using the db,
298  * which we can't allow, as that would open a new connection for mysql.
299  * Replace with a HashBag. They would not be going to persist anyway.
300  */
301  $hashCache = [ 'class' => HashBagOStuff::class, 'reportDupes' => false ];
302  $objectCaches = [
303  CACHE_DB => $hashCache,
304  CACHE_ACCEL => $hashCache,
305  CACHE_MEMCACHED => $hashCache,
306  'apc' => $hashCache,
307  'apcu' => $hashCache,
308  'wincache' => $hashCache,
309  ] + $baseConfig->get( 'ObjectCaches' );
310 
311  $defaultOverrides->set( 'ObjectCaches', $objectCaches );
312  $defaultOverrides->set( 'MainCacheType', CACHE_NONE );
313  $defaultOverrides->set( 'JobTypeConf', [ 'default' => [ 'class' => JobQueueMemory::class ] ] );
314 
315  // Use a fast hash algorithm to hash passwords.
316  $defaultOverrides->set( 'PasswordDefault', 'A' );
317 
318  $testConfig = $customOverrides
319  ? new MultiConfig( [ $customOverrides, $defaultOverrides, $baseConfig ] )
320  : new MultiConfig( [ $defaultOverrides, $baseConfig ] );
321 
322  return $testConfig;
323  }
324 
331  private static function makeTestConfigFactoryInstantiator(
332  ConfigFactory $oldFactory,
333  array $configurations
334  ) {
335  return function ( MediaWikiServices $services ) use ( $oldFactory, $configurations ) {
336  $factory = new ConfigFactory();
337 
338  // clone configurations from $oldFactory that are not overwritten by $configurations
339  $namesToClone = array_diff(
340  $oldFactory->getConfigNames(),
341  array_keys( $configurations )
342  );
343 
344  foreach ( $namesToClone as $name ) {
345  $factory->register( $name, $oldFactory->makeConfig( $name ) );
346  }
347 
348  foreach ( $configurations as $name => $config ) {
349  $factory->register( $name, $config );
350  }
351 
352  return $factory;
353  };
354  }
355 
360  public static function resetNonServiceCaches() {
361  global $wgRequest, $wgJobClasses;
362 
363  foreach ( $wgJobClasses as $type => $class ) {
364  JobQueueGroup::singleton()->get( $type )->delete();
365  }
367 
371 
372  // TODO: move global state into MediaWikiServices
374  if ( session_id() !== '' ) {
375  session_write_close();
376  session_id( '' );
377  }
378 
379  $wgRequest = new FauxRequest();
381  }
382 
383  public function run( PHPUnit_Framework_TestResult $result = null ) {
384  if ( $result instanceof MediaWikiTestResult ) {
385  $this->cliArgs = $result->getMediaWikiCliArgs();
386  }
387  $this->overrideMwServices();
388 
389  if ( $this->needsDB() && !$this->isTestInDatabaseGroup() ) {
390  throw new Exception(
391  get_class( $this ) . ' apparently needsDB but is not in the Database group'
392  );
393  }
394 
395  $needsResetDB = false;
396  if ( !self::$dbSetup || $this->needsDB() ) {
397  // set up a DB connection for this test to use
398 
399  self::$useTemporaryTables = !$this->getCliArg( 'use-normal-tables' );
400  self::$reuseDB = $this->getCliArg( 'reuse-db' );
401 
402  $this->db = wfGetDB( DB_MASTER );
403 
404  $this->checkDbIsSupported();
405 
406  if ( !self::$dbSetup ) {
407  $this->setupAllTestDBs();
408  $this->addCoreDBData();
409  }
410 
411  // TODO: the DB setup should be done in setUpBeforeClass(), so the test DB
412  // is available in subclass's setUpBeforeClass() and setUp() methods.
413  // This would also remove the need for the HACK that is oncePerClass().
414  if ( $this->oncePerClass() ) {
415  $this->setUpSchema( $this->db );
416  $this->resetDB( $this->db, $this->tablesUsed );
417  $this->addDBDataOnce();
418  }
419 
420  $this->addDBData();
421  $needsResetDB = true;
422  }
423 
424  parent::run( $result );
425 
426  // We don't mind if we override already-overridden services during cleanup
427  $this->overriddenServices = [];
428 
429  if ( $needsResetDB ) {
430  $this->resetDB( $this->db, $this->tablesUsed );
431  }
432 
433  self::restoreMwServices();
434  $this->localServices = null;
435  }
436 
440  private function oncePerClass() {
441  // Remember current test class in the database connection,
442  // so we know when we need to run addData.
443 
444  $class = static::class;
445 
446  $first = !isset( $this->db->_hasDataForTestClass )
447  || $this->db->_hasDataForTestClass !== $class;
448 
449  $this->db->_hasDataForTestClass = $class;
450  return $first;
451  }
452 
458  public function usesTemporaryTables() {
459  return self::$useTemporaryTables;
460  }
461 
471  protected function getNewTempFile() {
472  $fileName = tempnam( wfTempDir(), 'MW_PHPUnit_' . static::class . '_' );
473  $this->tmpFiles[] = $fileName;
474 
475  return $fileName;
476  }
477 
488  protected function getNewTempDirectory() {
489  // Starting of with a temporary /file/.
490  $fileName = $this->getNewTempFile();
491 
492  // Converting the temporary /file/ to a /directory/
493  // The following is not atomic, but at least we now have a single place,
494  // where temporary directory creation is bundled and can be improved
495  unlink( $fileName );
496  $this->assertTrue( wfMkdirParents( $fileName ) );
497 
498  return $fileName;
499  }
500 
501  protected function setUp() {
502  parent::setUp();
503  $this->called['setUp'] = true;
504 
505  $this->phpErrorLevel = intval( ini_get( 'error_reporting' ) );
506 
507  $this->overriddenServices = [];
508 
509  // Cleaning up temporary files
510  foreach ( $this->tmpFiles as $fileName ) {
511  if ( is_file( $fileName ) || ( is_link( $fileName ) ) ) {
512  unlink( $fileName );
513  } elseif ( is_dir( $fileName ) ) {
514  wfRecursiveRemoveDir( $fileName );
515  }
516  }
517 
518  if ( $this->needsDB() && $this->db ) {
519  // Clean up open transactions
520  while ( $this->db->trxLevel() > 0 ) {
521  $this->db->rollback( __METHOD__, 'flush' );
522  }
523  // Check for unsafe queries
524  if ( $this->db->getType() === 'mysql' ) {
525  $this->db->query( "SET sql_mode = 'STRICT_ALL_TABLES'", __METHOD__ );
526  }
527  }
528 
529  // Reset all caches between tests.
530  self::resetNonServiceCaches();
531 
532  // XXX: reset maintenance triggers
533  // Hook into period lag checks which often happen in long-running scripts
534  $lbFactory = $this->localServices->getDBLoadBalancerFactory();
535  Maintenance::setLBFactoryTriggers( $lbFactory, $this->localServices->getMainConfig() );
536 
537  ob_start( 'MediaWikiTestCase::wfResetOutputBuffersBarrier' );
538  }
539 
540  protected function addTmpFiles( $files ) {
541  $this->tmpFiles = array_merge( $this->tmpFiles, (array)$files );
542  }
543 
544  protected function tearDown() {
545  global $wgRequest, $wgSQLMode;
546 
547  $status = ob_get_status();
548  if ( isset( $status['name'] ) &&
549  $status['name'] === 'MediaWikiTestCase::wfResetOutputBuffersBarrier'
550  ) {
551  ob_end_flush();
552  }
553 
554  $this->called['tearDown'] = true;
555  // Cleaning up temporary files
556  foreach ( $this->tmpFiles as $fileName ) {
557  if ( is_file( $fileName ) || ( is_link( $fileName ) ) ) {
558  unlink( $fileName );
559  } elseif ( is_dir( $fileName ) ) {
560  wfRecursiveRemoveDir( $fileName );
561  }
562  }
563 
564  if ( $this->needsDB() && $this->db ) {
565  // Clean up open transactions
566  while ( $this->db->trxLevel() > 0 ) {
567  $this->db->rollback( __METHOD__, 'flush' );
568  }
569  if ( $this->db->getType() === 'mysql' ) {
570  $this->db->query( "SET sql_mode = " . $this->db->addQuotes( $wgSQLMode ),
571  __METHOD__ );
572  }
573  }
574 
575  // Re-enable any disabled deprecation warnings
577  // Restore mw globals
578  foreach ( $this->mwGlobals as $key => $value ) {
579  $GLOBALS[$key] = $value;
580  }
581  foreach ( $this->mwGlobalsToUnset as $value ) {
582  unset( $GLOBALS[$value] );
583  }
584  foreach ( $this->iniSettings as $name => $value ) {
585  ini_set( $name, $value );
586  }
587  if (
588  array_key_exists( 'wgExtraNamespaces', $this->mwGlobals ) ||
589  in_array( 'wgExtraNamespaces', $this->mwGlobalsToUnset )
590  ) {
591  $this->resetNamespaces();
592  }
593  $this->mwGlobals = [];
594  $this->mwGlobalsToUnset = [];
595  $this->restoreLoggers();
596 
597  // TODO: move global state into MediaWikiServices
599  if ( session_id() !== '' ) {
600  session_write_close();
601  session_id( '' );
602  }
603  $wgRequest = new FauxRequest();
606 
607  $phpErrorLevel = intval( ini_get( 'error_reporting' ) );
608 
609  if ( $phpErrorLevel !== $this->phpErrorLevel ) {
610  ini_set( 'error_reporting', $this->phpErrorLevel );
611 
612  $oldHex = strtoupper( dechex( $this->phpErrorLevel ) );
613  $newHex = strtoupper( dechex( $phpErrorLevel ) );
614  $message = "PHP error_reporting setting was left dirty: "
615  . "was 0x$oldHex before test, 0x$newHex after test!";
616 
617  $this->fail( $message );
618  }
619 
620  parent::tearDown();
621  }
622 
631  final public function testMediaWikiTestCaseParentSetupCalled() {
632  $this->assertArrayHasKey( 'setUp', $this->called,
633  static::class . '::setUp() must call parent::setUp()'
634  );
635  }
636 
646  protected function setService( $name, $object ) {
647  if ( !$this->localServices ) {
648  throw new Exception( __METHOD__ . ' must be called after MediaWikiTestCase::run()' );
649  }
650 
651  if ( $this->localServices !== MediaWikiServices::getInstance() ) {
652  throw new Exception( __METHOD__ . ' will not work because the global MediaWikiServices '
653  . 'instance has been replaced by test code.' );
654  }
655 
656  $this->overriddenServices[] = $name;
657 
658  $this->localServices->disableService( $name );
659  $this->localServices->redefineService(
660  $name,
661  function () use ( $object ) {
662  return $object;
663  }
664  );
665 
666  if ( $name === 'ContentLanguage' ) {
667  $this->doSetMwGlobals( [ 'wgContLang' => $object ] );
668  }
669  }
670 
706  protected function setMwGlobals( $pairs, $value = null ) {
707  if ( is_string( $pairs ) ) {
708  $pairs = [ $pairs => $value ];
709  }
710 
711  if ( isset( $pairs['wgContLang'] ) ) {
712  throw new MWException(
713  'No setting $wgContLang, use setContentLang() or setService( \'ContentLanguage\' )'
714  );
715  }
716 
717  $this->doSetMwGlobals( $pairs, $value );
718  }
719 
724  private function doSetMwGlobals( $pairs, $value = null ) {
725  $this->doStashMwGlobals( array_keys( $pairs ) );
726 
727  foreach ( $pairs as $key => $value ) {
728  $GLOBALS[$key] = $value;
729  }
730 
731  if ( array_key_exists( 'wgExtraNamespaces', $pairs ) ) {
732  $this->resetNamespaces();
733  }
734  }
735 
742  protected function setIniSetting( $name, $value ) {
743  $original = ini_get( $name );
744  $this->iniSettings[$name] = $original;
745  ini_set( $name, $value );
746  }
747 
752  private function resetNamespaces() {
753  if ( !$this->localServices ) {
754  throw new Exception( __METHOD__ . ' must be called after MediaWikiTestCase::run()' );
755  }
756 
757  if ( $this->localServices !== MediaWikiServices::getInstance() ) {
758  throw new Exception( __METHOD__ . ' will not work because the global MediaWikiServices '
759  . 'instance has been replaced by test code.' );
760  }
761 
764 
765  // We can't have the TitleFormatter holding on to an old Language object either
766  // @todo We shouldn't need to reset all the aliases here.
767  $this->localServices->resetServiceForTesting( 'TitleFormatter' );
768  $this->localServices->resetServiceForTesting( 'TitleParser' );
769  $this->localServices->resetServiceForTesting( '_MediaWikiTitleCodec' );
770  }
771 
780  private static function canShallowCopy( $value ) {
781  if ( is_scalar( $value ) || $value === null ) {
782  return true;
783  }
784  if ( is_array( $value ) ) {
785  foreach ( $value as $subValue ) {
786  if ( !is_scalar( $subValue ) && $subValue !== null ) {
787  return false;
788  }
789  }
790  return true;
791  }
792  return false;
793  }
794 
813  protected function stashMwGlobals( $globalKeys ) {
814  wfDeprecated( __METHOD__, '1.32' );
815  $this->doStashMwGlobals( $globalKeys );
816  }
817 
818  private function doStashMwGlobals( $globalKeys ) {
819  if ( is_string( $globalKeys ) ) {
820  $globalKeys = [ $globalKeys ];
821  }
822 
823  foreach ( $globalKeys as $globalKey ) {
824  // NOTE: make sure we only save the global once or a second call to
825  // setMwGlobals() on the same global would override the original
826  // value.
827  if (
828  !array_key_exists( $globalKey, $this->mwGlobals ) &&
829  !array_key_exists( $globalKey, $this->mwGlobalsToUnset )
830  ) {
831  if ( !array_key_exists( $globalKey, $GLOBALS ) ) {
832  $this->mwGlobalsToUnset[$globalKey] = $globalKey;
833  continue;
834  }
835  // NOTE: we serialize then unserialize the value in case it is an object
836  // this stops any objects being passed by reference. We could use clone
837  // and if is_object but this does account for objects within objects!
838  if ( self::canShallowCopy( $GLOBALS[$globalKey] ) ) {
839  $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
840  } elseif (
841  // Many MediaWiki types are safe to clone. These are the
842  // ones that are most commonly stashed.
843  $GLOBALS[$globalKey] instanceof Language ||
844  $GLOBALS[$globalKey] instanceof User ||
845  $GLOBALS[$globalKey] instanceof FauxRequest
846  ) {
847  $this->mwGlobals[$globalKey] = clone $GLOBALS[$globalKey];
848  } elseif ( $this->containsClosure( $GLOBALS[$globalKey] ) ) {
849  // Serializing Closure only gives a warning on HHVM while
850  // it throws an Exception on Zend.
851  // Workaround for https://github.com/facebook/hhvm/issues/6206
852  $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
853  } else {
854  try {
855  $this->mwGlobals[$globalKey] = unserialize( serialize( $GLOBALS[$globalKey] ) );
856  } catch ( Exception $e ) {
857  $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
858  }
859  }
860  }
861  }
862  }
863 
870  private function containsClosure( $var, $maxDepth = 15 ) {
871  if ( $var instanceof Closure ) {
872  return true;
873  }
874  if ( !is_array( $var ) || $maxDepth === 0 ) {
875  return false;
876  }
877 
878  foreach ( $var as $value ) {
879  if ( $this->containsClosure( $value, $maxDepth - 1 ) ) {
880  return true;
881  }
882  }
883  return false;
884  }
885 
901  protected function mergeMwGlobalArrayValue( $name, $values ) {
902  if ( !isset( $GLOBALS[$name] ) ) {
903  $merged = $values;
904  } else {
905  if ( !is_array( $GLOBALS[$name] ) ) {
906  throw new MWException( "MW global $name is not an array." );
907  }
908 
909  // NOTE: do not use array_merge, it screws up for numeric keys.
910  $merged = $GLOBALS[$name];
911  foreach ( $values as $k => $v ) {
912  $merged[$k] = $v;
913  }
914  }
915 
916  $this->setMwGlobals( $name, $merged );
917  }
918 
934  protected function overrideMwServices(
935  Config $configOverrides = null, array $services = []
936  ) {
937  if ( $this->overriddenServices ) {
938  throw new MWException(
939  'The following services were set and are now being unset by overrideMwServices: ' .
940  implode( ', ', $this->overriddenServices )
941  );
942  }
943  $newInstance = self::installMockMwServices( $configOverrides );
944 
945  if ( $this->localServices ) {
946  $this->localServices->destroy();
947  }
948 
949  $this->localServices = $newInstance;
950 
951  foreach ( $services as $name => $callback ) {
952  $newInstance->redefineService( $name, $callback );
953  }
954 
955  return $newInstance;
956  }
957 
975  public static function installMockMwServices( Config $configOverrides = null ) {
976  // Make sure we have the original service locator
977  if ( !self::$originalServices ) {
978  self::$originalServices = MediaWikiServices::getInstance();
979  }
980 
981  if ( !$configOverrides ) {
982  $configOverrides = new HashConfig();
983  }
984 
985  $oldConfigFactory = self::$originalServices->getConfigFactory();
986  $oldLoadBalancerFactory = self::$originalServices->getDBLoadBalancerFactory();
987 
988  $testConfig = self::makeTestConfig( null, $configOverrides );
989  $newServices = new MediaWikiServices( $testConfig );
990 
991  // Load the default wiring from the specified files.
992  // NOTE: this logic mirrors the logic in MediaWikiServices::newInstance.
993  $wiringFiles = $testConfig->get( 'ServiceWiringFiles' );
994  $newServices->loadWiringFiles( $wiringFiles );
995 
996  // Provide a traditional hook point to allow extensions to configure services.
997  Hooks::run( 'MediaWikiServices', [ $newServices ] );
998 
999  // Use bootstrap config for all configuration.
1000  // This allows config overrides via global variables to take effect.
1001  $bootstrapConfig = $newServices->getBootstrapConfig();
1002  $newServices->resetServiceForTesting( 'ConfigFactory' );
1003  $newServices->redefineService(
1004  'ConfigFactory',
1005  self::makeTestConfigFactoryInstantiator(
1006  $oldConfigFactory,
1007  [ 'main' => $bootstrapConfig ]
1008  )
1009  );
1010  $newServices->resetServiceForTesting( 'DBLoadBalancerFactory' );
1011  $newServices->redefineService(
1012  'DBLoadBalancerFactory',
1013  function ( MediaWikiServices $services ) use ( $oldLoadBalancerFactory ) {
1014  return $oldLoadBalancerFactory;
1015  }
1016  );
1017 
1018  MediaWikiServices::forceGlobalInstance( $newServices );
1019  return $newServices;
1020  }
1021 
1033  public static function restoreMwServices() {
1034  if ( !self::$originalServices ) {
1035  return false;
1036  }
1037 
1038  $currentServices = MediaWikiServices::getInstance();
1039 
1040  if ( self::$originalServices === $currentServices ) {
1041  return false;
1042  }
1043 
1044  MediaWikiServices::forceGlobalInstance( self::$originalServices );
1045  $currentServices->destroy();
1046 
1047  return true;
1048  }
1049 
1054  public function setUserLang( $lang ) {
1055  RequestContext::getMain()->setLanguage( $lang );
1056  $this->setMwGlobals( 'wgLang', RequestContext::getMain()->getLanguage() );
1057  }
1058 
1063  public function setContentLang( $lang ) {
1064  if ( $lang instanceof Language ) {
1065  $this->setMwGlobals( 'wgLanguageCode', $lang->getCode() );
1066  // Set to the exact object requested
1067  $this->setService( 'ContentLanguage', $lang );
1068  } else {
1069  $this->setMwGlobals( 'wgLanguageCode', $lang );
1070  // Let the service handler make up the object. Avoid calling setService(), because if
1071  // we do, overrideMwServices() will complain if it's called later on.
1072  $services = MediaWikiServices::getInstance();
1073  $services->resetServiceForTesting( 'ContentLanguage' );
1074  $this->doSetMwGlobals( [ 'wgContLang' => $services->getContentLanguage() ] );
1075  }
1076  }
1077 
1092  public function setGroupPermissions( $newPerms, $newKey = null, $newValue = null ) {
1093  global $wgGroupPermissions;
1094 
1095  if ( is_string( $newPerms ) ) {
1096  $newPerms = [ $newPerms => [ $newKey => $newValue ] ];
1097  }
1098 
1099  $newPermissions = $wgGroupPermissions;
1100  foreach ( $newPerms as $group => $permissions ) {
1101  foreach ( $permissions as $key => $value ) {
1102  $newPermissions[$group][$key] = $value;
1103  }
1104  }
1105 
1106  $this->setMwGlobals( 'wgGroupPermissions', $newPermissions );
1107  }
1108 
1115  protected function setLogger( $channel, LoggerInterface $logger ) {
1116  // TODO: Once loggers are managed by MediaWikiServices, use
1117  // overrideMwServices() to set loggers.
1118 
1119  $provider = LoggerFactory::getProvider();
1120  $wrappedProvider = TestingAccessWrapper::newFromObject( $provider );
1121  $singletons = $wrappedProvider->singletons;
1122  if ( $provider instanceof MonologSpi ) {
1123  if ( !isset( $this->loggers[$channel] ) ) {
1124  $this->loggers[$channel] = $singletons['loggers'][$channel] ?? null;
1125  }
1126  $singletons['loggers'][$channel] = $logger;
1127  } elseif ( $provider instanceof LegacySpi ) {
1128  if ( !isset( $this->loggers[$channel] ) ) {
1129  $this->loggers[$channel] = $singletons[$channel] ?? null;
1130  }
1131  $singletons[$channel] = $logger;
1132  } else {
1133  throw new LogicException( __METHOD__ . ': setting a logger for ' . get_class( $provider )
1134  . ' is not implemented' );
1135  }
1136  $wrappedProvider->singletons = $singletons;
1137  }
1138 
1143  private function restoreLoggers() {
1144  $provider = LoggerFactory::getProvider();
1145  $wrappedProvider = TestingAccessWrapper::newFromObject( $provider );
1146  $singletons = $wrappedProvider->singletons;
1147  foreach ( $this->loggers as $channel => $logger ) {
1148  if ( $provider instanceof MonologSpi ) {
1149  if ( $logger === null ) {
1150  unset( $singletons['loggers'][$channel] );
1151  } else {
1152  $singletons['loggers'][$channel] = $logger;
1153  }
1154  } elseif ( $provider instanceof LegacySpi ) {
1155  if ( $logger === null ) {
1156  unset( $singletons[$channel] );
1157  } else {
1158  $singletons[$channel] = $logger;
1159  }
1160  }
1161  }
1162  $wrappedProvider->singletons = $singletons;
1163  $this->loggers = [];
1164  }
1165 
1170  public function dbPrefix() {
1171  return self::getTestPrefixFor( $this->db );
1172  }
1173 
1179  public static function getTestPrefixFor( IDatabase $db ) {
1180  return $db->getType() == 'oracle' ? self::ORA_DB_PREFIX : self::DB_PREFIX;
1181  }
1182 
1187  public function needsDB() {
1188  // If the test says it uses database tables, it needs the database
1189  return $this->tablesUsed || $this->isTestInDatabaseGroup();
1190  }
1191 
1196  protected function isTestInDatabaseGroup() {
1197  // If the test class says it belongs to the Database group, it needs the database.
1198  // NOTE: This ONLY checks for the group in the class level doc comment.
1199  $rc = new ReflectionClass( $this );
1200  return (bool)preg_match( '/@group +Database/im', $rc->getDocComment() );
1201  }
1202 
1219  protected function insertPage(
1220  $pageName,
1221  $text = 'Sample page for unit test.',
1222  $namespace = null,
1223  User $user = null
1224  ) {
1225  if ( !$this->needsDB() ) {
1226  throw new MWException( 'When testing which pages, the test cases\'s needsDB()' .
1227  ' method should return true. Use @group Database or $this->tablesUsed.' );
1228  }
1229 
1230  if ( is_string( $pageName ) ) {
1231  $title = Title::newFromText( $pageName, $namespace );
1232  } else {
1233  $title = $pageName;
1234  }
1235 
1236  if ( !$user ) {
1237  $user = static::getTestSysop()->getUser();
1238  }
1239  $comment = __METHOD__ . ': Sample page for unit test.';
1240 
1241  $page = WikiPage::factory( $title );
1242  $page->doEditContent( ContentHandler::makeContent( $text, $title ), $comment, 0, false, $user );
1243 
1244  return [
1245  'title' => $title,
1246  'id' => $page->getId(),
1247  ];
1248  }
1249 
1265  public function addDBDataOnce() {
1266  }
1267 
1277  public function addDBData() {
1278  }
1279 
1283  protected function addCoreDBData() {
1284  if ( $this->db->getType() == 'oracle' ) {
1285  # Insert 0 user to prevent FK violations
1286  # Anonymous user
1287  if ( !$this->db->selectField( 'user', '1', [ 'user_id' => 0 ] ) ) {
1288  $this->db->insert( 'user', [
1289  'user_id' => 0,
1290  'user_name' => 'Anonymous' ], __METHOD__, [ 'IGNORE' ] );
1291  }
1292 
1293  # Insert 0 page to prevent FK violations
1294  # Blank page
1295  if ( !$this->db->selectField( 'page', '1', [ 'page_id' => 0 ] ) ) {
1296  $this->db->insert( 'page', [
1297  'page_id' => 0,
1298  'page_namespace' => 0,
1299  'page_title' => ' ',
1300  'page_restrictions' => null,
1301  'page_is_redirect' => 0,
1302  'page_is_new' => 0,
1303  'page_random' => 0,
1304  'page_touched' => $this->db->timestamp(),
1305  'page_latest' => 0,
1306  'page_len' => 0 ], __METHOD__, [ 'IGNORE' ] );
1307  }
1308  }
1309 
1311 
1313 
1314  // Make sysop user
1315  $user = static::getTestSysop()->getUser();
1316 
1317  // Make 1 page with 1 revision
1318  $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
1319  if ( $page->getId() == 0 ) {
1320  $page->doEditContent(
1321  new WikitextContent( 'UTContent' ),
1322  'UTPageSummary',
1324  false,
1325  $user
1326  );
1327  // an edit always attempt to purge backlink links such as history
1328  // pages. That is unnecessary.
1329  JobQueueGroup::singleton()->get( 'htmlCacheUpdate' )->delete();
1330  // WikiPages::doEditUpdates randomly adds RC purges
1331  JobQueueGroup::singleton()->get( 'recentChangesUpdate' )->delete();
1332 
1333  // doEditContent() probably started the session via
1334  // User::loadFromSession(). Close it now.
1335  if ( session_id() !== '' ) {
1336  session_write_close();
1337  session_id( '' );
1338  }
1339  }
1340  }
1341 
1351  public static function teardownTestDB() {
1352  global $wgJobClasses;
1353 
1354  if ( !self::$dbSetup ) {
1355  return;
1356  }
1357 
1358  Hooks::run( 'UnitTestsBeforeDatabaseTeardown' );
1359 
1360  foreach ( $wgJobClasses as $type => $class ) {
1361  // Delete any jobs under the clone DB (or old prefix in other stores)
1362  JobQueueGroup::singleton()->get( $type )->delete();
1363  }
1364 
1365  CloneDatabase::changePrefix( self::$oldTablePrefix );
1366 
1367  self::$oldTablePrefix = false;
1368  self::$dbSetup = false;
1369  }
1370 
1382  protected static function setupDatabaseWithTestPrefix(
1384  $prefix = null
1385  ) {
1386  if ( $prefix === null ) {
1387  $prefix = self::getTestPrefixFor( $db );
1388  }
1389 
1390  if ( ( $db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
1391  $db->tablePrefix( $prefix );
1392  return false;
1393  }
1394 
1395  if ( !isset( $db->_originalTablePrefix ) ) {
1396  $oldPrefix = $db->tablePrefix();
1397 
1398  if ( $oldPrefix === $prefix ) {
1399  // table already has the correct prefix, but presumably no cloned tables
1400  $oldPrefix = self::$oldTablePrefix;
1401  }
1402 
1403  $db->tablePrefix( $oldPrefix );
1404  $tablesCloned = self::listTables( $db );
1405  $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix, $oldPrefix );
1406  $dbClone->useTemporaryTables( self::$useTemporaryTables );
1407 
1408  $dbClone->cloneTableStructure();
1409 
1410  $db->tablePrefix( $prefix );
1411  $db->_originalTablePrefix = $oldPrefix;
1412  }
1413 
1414  return true;
1415  }
1416 
1420  public function setupAllTestDBs() {
1421  global $wgDBprefix;
1422 
1423  self::$oldTablePrefix = $wgDBprefix;
1424 
1425  $testPrefix = $this->dbPrefix();
1426 
1427  // switch to a temporary clone of the database
1428  self::setupTestDB( $this->db, $testPrefix );
1429 
1430  if ( self::isUsingExternalStoreDB() ) {
1431  self::setupExternalStoreTestDBs( $testPrefix );
1432  }
1433 
1434  // NOTE: Change the prefix in the LBFactory and $wgDBprefix, to prevent
1435  // *any* database connections to operate on live data.
1436  CloneDatabase::changePrefix( $testPrefix );
1437  }
1438 
1460  public static function setupTestDB( Database $db, $prefix ) {
1461  if ( self::$dbSetup ) {
1462  return;
1463  }
1464 
1465  if ( $db->tablePrefix() === $prefix ) {
1466  throw new MWException(
1467  'Cannot run unit tests, the database prefix is already "' . $prefix . '"' );
1468  }
1469 
1470  // TODO: the below should be re-written as soon as LBFactory, LoadBalancer,
1471  // and Database no longer use global state.
1472 
1473  self::$dbSetup = true;
1474 
1475  if ( !self::setupDatabaseWithTestPrefix( $db, $prefix ) ) {
1476  return;
1477  }
1478 
1479  // Assuming this isn't needed for External Store database, and not sure if the procedure
1480  // would be available there.
1481  if ( $db->getType() == 'oracle' ) {
1482  $db->query( 'BEGIN FILL_WIKI_INFO; END;', __METHOD__ );
1483  }
1484 
1485  Hooks::run( 'UnitTestsAfterDatabaseSetup', [ $db, $prefix ] );
1486  }
1487 
1494  protected static function setupExternalStoreTestDBs( $testPrefix = null ) {
1495  $connections = self::getExternalStoreDatabaseConnections();
1496  foreach ( $connections as $dbw ) {
1497  self::setupDatabaseWithTestPrefix( $dbw, $testPrefix );
1498  }
1499  }
1500 
1507  protected static function getExternalStoreDatabaseConnections() {
1508  global $wgDefaultExternalStore;
1509 
1511  $externalStoreDB = ExternalStore::getStoreObject( 'DB' );
1512  $defaultArray = (array)$wgDefaultExternalStore;
1513  $dbws = [];
1514  foreach ( $defaultArray as $url ) {
1515  if ( strpos( $url, 'DB://' ) === 0 ) {
1516  list( $proto, $cluster ) = explode( '://', $url, 2 );
1517  // Avoid getMaster() because setupDatabaseWithTestPrefix()
1518  // requires Database instead of plain DBConnRef/IDatabase
1519  $dbws[] = $externalStoreDB->getMaster( $cluster );
1520  }
1521  }
1522 
1523  return $dbws;
1524  }
1525 
1531  protected static function isUsingExternalStoreDB() {
1532  global $wgDefaultExternalStore;
1533  if ( !$wgDefaultExternalStore ) {
1534  return false;
1535  }
1536 
1537  $defaultArray = (array)$wgDefaultExternalStore;
1538  foreach ( $defaultArray as $url ) {
1539  if ( strpos( $url, 'DB://' ) === 0 ) {
1540  return true;
1541  }
1542  }
1543 
1544  return false;
1545  }
1546 
1554  if ( $db->tablePrefix() !== $this->dbPrefix() ) {
1555  throw new LogicException(
1556  'Trying to delete mock tables, but table prefix does not indicate a mock database.'
1557  );
1558  }
1559  }
1560 
1561  private static $schemaOverrideDefaults = [
1562  'scripts' => [],
1563  'create' => [],
1564  'drop' => [],
1565  'alter' => [],
1566  ];
1567 
1583  return [];
1584  }
1585 
1593  private function undoSchemaOverrides( IMaintainableDatabase $db, $oldOverrides ) {
1594  $this->ensureMockDatabaseConnection( $db );
1595 
1596  $oldOverrides = $oldOverrides + self::$schemaOverrideDefaults;
1597  $originalTables = $this->listOriginalTables( $db, 'unprefixed' );
1598 
1599  // Drop tables that need to be restored or removed.
1600  $tablesToDrop = array_merge( $oldOverrides['create'], $oldOverrides['alter'] );
1601 
1602  // Restore tables that have been dropped or created or altered,
1603  // if they exist in the original schema.
1604  $tablesToRestore = array_merge( $tablesToDrop, $oldOverrides['drop'] );
1605  $tablesToRestore = array_intersect( $originalTables, $tablesToRestore );
1606 
1607  if ( $tablesToDrop ) {
1608  $this->dropMockTables( $db, $tablesToDrop );
1609  }
1610 
1611  if ( $tablesToRestore ) {
1612  $this->recloneMockTables( $db, $tablesToRestore );
1613  }
1614  }
1615 
1621  private function setUpSchema( IMaintainableDatabase $db ) {
1622  // Undo any active overrides.
1623  $oldOverrides = $db->_schemaOverrides ?? self::$schemaOverrideDefaults;
1624 
1625  if ( $oldOverrides['alter'] || $oldOverrides['create'] || $oldOverrides['drop'] ) {
1626  $this->undoSchemaOverrides( $db, $oldOverrides );
1627  }
1628 
1629  // Determine new overrides.
1630  $overrides = $this->getSchemaOverrides( $db ) + self::$schemaOverrideDefaults;
1631 
1632  $extraKeys = array_diff(
1633  array_keys( $overrides ),
1634  array_keys( self::$schemaOverrideDefaults )
1635  );
1636 
1637  if ( $extraKeys ) {
1638  throw new InvalidArgumentException(
1639  'Schema override contains extra keys: ' . var_export( $extraKeys, true )
1640  );
1641  }
1642 
1643  if ( !$overrides['scripts'] ) {
1644  // no scripts to run
1645  return;
1646  }
1647 
1648  if ( !$overrides['create'] && !$overrides['drop'] && !$overrides['alter'] ) {
1649  throw new InvalidArgumentException(
1650  'Schema override scripts given, but no tables are declared to be '
1651  . 'created, dropped or altered.'
1652  );
1653  }
1654 
1655  $this->ensureMockDatabaseConnection( $db );
1656 
1657  // Drop the tables that will be created by the schema scripts.
1658  $originalTables = $this->listOriginalTables( $db, 'unprefixed' );
1659  $tablesToDrop = array_intersect( $originalTables, $overrides['create'] );
1660 
1661  if ( $tablesToDrop ) {
1662  $this->dropMockTables( $db, $tablesToDrop );
1663  }
1664 
1665  // Run schema override scripts.
1666  foreach ( $overrides['scripts'] as $script ) {
1667  $db->sourceFile(
1668  $script,
1669  null,
1670  null,
1671  __METHOD__,
1672  function ( $cmd ) {
1673  return $this->mungeSchemaUpdateQuery( $cmd );
1674  }
1675  );
1676  }
1677 
1678  $db->_schemaOverrides = $overrides;
1679  }
1680 
1681  private function mungeSchemaUpdateQuery( $cmd ) {
1682  return self::$useTemporaryTables
1683  ? preg_replace( '/\bCREATE\s+TABLE\b/i', 'CREATE TEMPORARY TABLE', $cmd )
1684  : $cmd;
1685  }
1686 
1694  $this->ensureMockDatabaseConnection( $db );
1695 
1696  foreach ( $tables as $tbl ) {
1697  $tbl = $db->tableName( $tbl );
1698  $db->query( "DROP TABLE IF EXISTS $tbl", __METHOD__ );
1699  }
1700  }
1701 
1709  private function listOriginalTables( IMaintainableDatabase $db, $prefix = 'prefixed' ) {
1710  if ( !isset( $db->_originalTablePrefix ) ) {
1711  throw new LogicException( 'No original table prefix know, cannot list tables!' );
1712  }
1713 
1714  $originalTables = $db->listTables( $db->_originalTablePrefix, __METHOD__ );
1715  if ( $prefix === 'unprefixed' ) {
1716  $originalPrefixRegex = '/^' . preg_quote( $db->_originalTablePrefix, '/' ) . '/';
1717  $originalTables = array_map(
1718  function ( $pt ) use ( $originalPrefixRegex ) {
1719  return preg_replace( $originalPrefixRegex, '', $pt );
1720  },
1721  $originalTables
1722  );
1723  }
1724 
1725  return $originalTables;
1726  }
1727 
1737  $this->ensureMockDatabaseConnection( $db );
1738 
1739  if ( !isset( $db->_originalTablePrefix ) ) {
1740  throw new LogicException( 'No original table prefix know, cannot restore tables!' );
1741  }
1742 
1743  $originalTables = $this->listOriginalTables( $db, 'unprefixed' );
1744  $tables = array_intersect( $tables, $originalTables );
1745 
1746  $dbClone = new CloneDatabase( $db, $tables, $db->tablePrefix(), $db->_originalTablePrefix );
1747  $dbClone->useTemporaryTables( self::$useTemporaryTables );
1748 
1749  $dbClone->cloneTableStructure();
1750  }
1751 
1758  private function resetDB( $db, $tablesUsed ) {
1759  if ( $db ) {
1760  $userTables = [ 'user', 'user_groups', 'user_properties', 'actor' ];
1761  $pageTables = [
1762  'page', 'revision', 'ip_changes', 'revision_comment_temp', 'comment', 'archive',
1763  'revision_actor_temp', 'slots', 'content', 'content_models', 'slot_roles',
1764  ];
1765  $coreDBDataTables = array_merge( $userTables, $pageTables );
1766 
1767  // If any of the user or page tables were marked as used, we should clear all of them.
1768  if ( array_intersect( $tablesUsed, $userTables ) ) {
1769  $tablesUsed = array_unique( array_merge( $tablesUsed, $userTables ) );
1771  }
1772  if ( array_intersect( $tablesUsed, $pageTables ) ) {
1773  $tablesUsed = array_unique( array_merge( $tablesUsed, $pageTables ) );
1774  }
1775 
1776  // Postgres, Oracle, and MSSQL all use mwuser/pagecontent
1777  // instead of user/text. But Postgres does not remap the
1778  // table name in tableExists(), so we mark the real table
1779  // names as being used.
1780  if ( $db->getType() === 'postgres' ) {
1781  if ( in_array( 'user', $tablesUsed ) ) {
1782  $tablesUsed[] = 'mwuser';
1783  }
1784  if ( in_array( 'text', $tablesUsed ) ) {
1785  $tablesUsed[] = 'pagecontent';
1786  }
1787  }
1788 
1789  foreach ( $tablesUsed as $tbl ) {
1790  $this->truncateTable( $tbl, $db );
1791  }
1792 
1793  if ( array_intersect( $tablesUsed, $coreDBDataTables ) ) {
1794  // Reset services that may contain information relating to the truncated tables
1795  $this->overrideMwServices();
1796  // Re-add core DB data that was deleted
1797  $this->addCoreDBData();
1798  }
1799  }
1800  }
1801 
1810  protected function truncateTable( $tableName, IDatabase $db = null ) {
1811  if ( !$db ) {
1812  $db = $this->db;
1813  }
1814 
1815  if ( !$db->tableExists( $tableName ) ) {
1816  return;
1817  }
1818 
1819  $truncate = in_array( $db->getType(), [ 'oracle', 'mysql' ] );
1820 
1821  if ( $truncate ) {
1822  $db->query( 'TRUNCATE TABLE ' . $db->tableName( $tableName ), __METHOD__ );
1823  } else {
1824  $db->delete( $tableName, '*', __METHOD__ );
1825  }
1826 
1827  if ( $db instanceof DatabasePostgres || $db instanceof DatabaseSqlite ) {
1828  // Reset the table's sequence too.
1829  $db->resetSequenceForTable( $tableName, __METHOD__ );
1830  }
1831 
1832  // re-initialize site_stats table
1833  if ( $tableName === 'site_stats' ) {
1835  }
1836  }
1837 
1838  private static function unprefixTable( &$tableName, $ind, $prefix ) {
1839  $tableName = substr( $tableName, strlen( $prefix ) );
1840  }
1841 
1842  private static function isNotUnittest( $table ) {
1843  return strpos( $table, self::DB_PREFIX ) !== 0;
1844  }
1845 
1853  public static function listTables( IMaintainableDatabase $db ) {
1854  $prefix = $db->tablePrefix();
1855  $tables = $db->listTables( $prefix, __METHOD__ );
1856 
1857  if ( $db->getType() === 'mysql' ) {
1858  static $viewListCache = null;
1859  if ( $viewListCache === null ) {
1860  $viewListCache = $db->listViews( null, __METHOD__ );
1861  }
1862  // T45571: cannot clone VIEWs under MySQL
1863  $tables = array_diff( $tables, $viewListCache );
1864  }
1865  array_walk( $tables, [ __CLASS__, 'unprefixTable' ], $prefix );
1866 
1867  // Don't duplicate test tables from the previous fataled run
1868  $tables = array_filter( $tables, [ __CLASS__, 'isNotUnittest' ] );
1869 
1870  if ( $db->getType() == 'sqlite' ) {
1871  $tables = array_flip( $tables );
1872  // these are subtables of searchindex and don't need to be duped/dropped separately
1873  unset( $tables['searchindex_content'] );
1874  unset( $tables['searchindex_segdir'] );
1875  unset( $tables['searchindex_segments'] );
1876  $tables = array_flip( $tables );
1877  }
1878 
1879  return $tables;
1880  }
1881 
1890  public function copyTestData( IDatabase $source, IDatabase $target ) {
1891  $tables = self::listOriginalTables( $source, 'unprefixed' );
1892 
1893  foreach ( $tables as $table ) {
1894  $res = $source->select( $table, '*', [], __METHOD__ );
1895  $allRows = [];
1896 
1897  foreach ( $res as $row ) {
1898  $allRows[] = (array)$row;
1899  }
1900 
1901  $target->insert( $table, $allRows, __METHOD__, [ 'IGNORE' ] );
1902  }
1903  }
1904 
1909  protected function checkDbIsSupported() {
1910  if ( !in_array( $this->db->getType(), $this->supportedDBs ) ) {
1911  throw new MWException( $this->db->getType() . " is not currently supported for unit testing." );
1912  }
1913  }
1914 
1920  public function getCliArg( $offset ) {
1921  return $this->cliArgs[$offset] ?? null;
1922  }
1923 
1929  public function setCliArg( $offset, $value ) {
1930  $this->cliArgs[$offset] = $value;
1931  }
1932 
1940  public function hideDeprecated( $function ) {
1941  Wikimedia\suppressWarnings();
1942  wfDeprecated( $function );
1943  Wikimedia\restoreWarnings();
1944  }
1945 
1966  protected function assertSelect(
1967  $table, $fields, $condition, array $expectedRows, array $options = [], array $join_conds = []
1968  ) {
1969  if ( !$this->needsDB() ) {
1970  throw new MWException( 'When testing database state, the test cases\'s needDB()' .
1971  ' method should return true. Use @group Database or $this->tablesUsed.' );
1972  }
1973 
1974  $db = wfGetDB( DB_REPLICA );
1975 
1976  $res = $db->select(
1977  $table,
1978  $fields,
1979  $condition,
1980  wfGetCaller(),
1981  $options + [ 'ORDER BY' => $fields ],
1982  $join_conds
1983  );
1984  $this->assertNotEmpty( $res, "query failed: " . $db->lastError() );
1985 
1986  $i = 0;
1987 
1988  foreach ( $expectedRows as $expected ) {
1989  $r = $res->fetchRow();
1990  self::stripStringKeys( $r );
1991 
1992  $i += 1;
1993  $this->assertNotEmpty( $r, "row #$i missing" );
1994 
1995  $this->assertEquals( $expected, $r, "row #$i mismatches" );
1996  }
1997 
1998  $r = $res->fetchRow();
1999  self::stripStringKeys( $r );
2000 
2001  $this->assertFalse( $r, "found extra row (after #$i)" );
2002  }
2003 
2015  protected function arrayWrap( array $elements ) {
2016  return array_map(
2017  function ( $element ) {
2018  return [ $element ];
2019  },
2020  $elements
2021  );
2022  }
2023 
2036  protected function assertArrayEquals( array $expected, array $actual,
2037  $ordered = false, $named = false
2038  ) {
2039  if ( !$ordered ) {
2040  $this->objectAssociativeSort( $expected );
2041  $this->objectAssociativeSort( $actual );
2042  }
2043 
2044  if ( !$named ) {
2045  $expected = array_values( $expected );
2046  $actual = array_values( $actual );
2047  }
2048 
2049  call_user_func_array(
2050  [ $this, 'assertEquals' ],
2051  array_merge( [ $expected, $actual ], array_slice( func_get_args(), 4 ) )
2052  );
2053  }
2054 
2067  protected function assertHTMLEquals( $expected, $actual, $msg = '' ) {
2068  $expected = str_replace( '>', ">\n", $expected );
2069  $actual = str_replace( '>', ">\n", $actual );
2070 
2071  $this->assertEquals( $expected, $actual, $msg );
2072  }
2073 
2081  protected function objectAssociativeSort( array &$array ) {
2082  uasort(
2083  $array,
2084  function ( $a, $b ) {
2085  return serialize( $a ) <=> serialize( $b );
2086  }
2087  );
2088  }
2089 
2099  protected static function stripStringKeys( &$r ) {
2100  if ( !is_array( $r ) ) {
2101  return;
2102  }
2103 
2104  foreach ( $r as $k => $v ) {
2105  if ( is_string( $k ) ) {
2106  unset( $r[$k] );
2107  }
2108  }
2109  }
2110 
2124  protected function assertTypeOrValue( $type, $actual, $value = false, $message = '' ) {
2125  if ( $actual === $value ) {
2126  $this->assertTrue( true, $message );
2127  } else {
2128  $this->assertType( $type, $actual, $message );
2129  }
2130  }
2131 
2143  protected function assertType( $type, $actual, $message = '' ) {
2144  if ( class_exists( $type ) || interface_exists( $type ) ) {
2145  $this->assertInstanceOf( $type, $actual, $message );
2146  } else {
2147  $this->assertInternalType( $type, $actual, $message );
2148  }
2149  }
2150 
2160  protected function isWikitextNS( $ns ) {
2162 
2163  if ( isset( $wgNamespaceContentModels[$ns] ) ) {
2164  return $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT;
2165  }
2166 
2167  return true;
2168  }
2169 
2177  protected function getDefaultWikitextNS() {
2179 
2180  static $wikitextNS = null; // this is not going to change
2181  if ( $wikitextNS !== null ) {
2182  return $wikitextNS;
2183  }
2184 
2185  // quickly short out on most common case:
2186  if ( !isset( $wgNamespaceContentModels[NS_MAIN] ) ) {
2187  return NS_MAIN;
2188  }
2189 
2190  // NOTE: prefer content namespaces
2191  $namespaces = array_unique( array_merge(
2193  [ NS_MAIN, NS_HELP, NS_PROJECT ], // prefer these
2195  ) );
2196 
2197  $namespaces = array_diff( $namespaces, [
2198  NS_FILE, NS_CATEGORY, NS_MEDIAWIKI, NS_USER // don't mess with magic namespaces
2199  ] );
2200 
2201  $talk = array_filter( $namespaces, function ( $ns ) {
2202  return MWNamespace::isTalk( $ns );
2203  } );
2204 
2205  // prefer non-talk pages
2206  $namespaces = array_diff( $namespaces, $talk );
2207  $namespaces = array_merge( $namespaces, $talk );
2208 
2209  // check default content model of each namespace
2210  foreach ( $namespaces as $ns ) {
2211  if ( !isset( $wgNamespaceContentModels[$ns] ) ||
2212  $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT
2213  ) {
2214  $wikitextNS = $ns;
2215 
2216  return $wikitextNS;
2217  }
2218  }
2219 
2220  // give up
2221  // @todo Inside a test, we could skip the test as incomplete.
2222  // But frequently, this is used in fixture setup.
2223  throw new MWException( "No namespace defaults to wikitext!" );
2224  }
2225 
2232  protected function markTestSkippedIfNoDiff3() {
2233  global $wgDiff3;
2234 
2235  # This check may also protect against code injection in
2236  # case of broken installations.
2237  Wikimedia\suppressWarnings();
2238  $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 );
2239  Wikimedia\restoreWarnings();
2240 
2241  if ( !$haveDiff3 ) {
2242  $this->markTestSkipped( "Skip test, since diff3 is not configured" );
2243  }
2244  }
2245 
2254  protected function checkPHPExtension( $extName ) {
2255  $loaded = extension_loaded( $extName );
2256  if ( !$loaded ) {
2257  $this->markTestSkipped( "PHP extension '$extName' is not loaded, skipping." );
2258  }
2259 
2260  return $loaded;
2261  }
2262 
2269  protected function markTestSkippedIfDbType( $type ) {
2270  if ( $this->db->getType() === $type ) {
2271  $this->markTestSkipped( "The $type database type isn't supported for this test" );
2272  }
2273  }
2274 
2280  public static function wfResetOutputBuffersBarrier( $buffer ) {
2281  return $buffer;
2282  }
2283 
2291  protected function setTemporaryHook( $hookName, $handler ) {
2292  $this->mergeMwGlobalArrayValue( 'wgHooks', [ $hookName => [ $handler ] ] );
2293  }
2294 
2304  protected function assertFileContains(
2305  $fileName,
2306  $actualData,
2307  $createIfMissing = false,
2308  $msg = ''
2309  ) {
2310  if ( $createIfMissing ) {
2311  if ( !file_exists( $fileName ) ) {
2312  file_put_contents( $fileName, $actualData );
2313  $this->markTestSkipped( 'Data file $fileName does not exist' );
2314  }
2315  } else {
2316  self::assertFileExists( $fileName );
2317  }
2318  self::assertEquals( file_get_contents( $fileName ), $actualData, $msg );
2319  }
2320 
2333  protected function editPage( $pageName, $text, $summary = '', $defaultNs = NS_MAIN ) {
2334  if ( !$this->needsDB() ) {
2335  throw new MWException( 'When testing which pages, the test cases\'s needsDB()' .
2336  ' method should return true. Use @group Database or $this->tablesUsed.' );
2337  }
2338 
2339  $title = Title::newFromText( $pageName, $defaultNs );
2340  $page = WikiPage::factory( $title );
2341 
2342  return $page->doEditContent( ContentHandler::makeContent( $text, $title ), $summary );
2343  }
2344 
2353  protected function revisionDelete(
2354  $rev, array $value = [ Revision::DELETED_TEXT => 1 ], $comment = ''
2355  ) {
2356  if ( is_int( $rev ) ) {
2358  }
2360  'revision', RequestContext::getMain(), $rev->getTitle(), [ $rev->getId() ]
2361  )->setVisibility( [
2362  'value' => $value,
2363  'comment' => $comment,
2364  ] );
2365  }
2366 }
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:128
const DB_PREFIX
Table name prefixes.
string [] $overriddenServices
Holds a list of services that were overridden with setService().
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
LoggerInterface [] $loggers
Holds original loggers which have been replaced by setLogger()
static clearPendingUpdates()
Clear all pending updates without performing them.
setTemporaryHook( $hookName, $handler)
Create a temporary hook handler which will be reset by tearDown.
setCliArg( $offset, $value)
listViews( $prefix=null, $fname=__METHOD__)
Lists all the VIEWs in the database.
static doPlaceholderInit()
Insert a dummy row with all zeroes if no row is present.
const CONTENT_MODEL_WIKITEXT
Definition: Defines.php:235
static setupDatabaseWithTestPrefix(IMaintainableDatabase $db, $prefix=null)
Setups a database with cloned tables using the given prefix.
array $wgDefaultExternalStore
The place to put new revisions, false to put them in the local text table.
assertType( $type, $actual, $message='')
Asserts the type of the provided value.
ensureMockDatabaseConnection(IDatabase $db)
query( $sql, $fname=__METHOD__, $tempIgnore=false)
Run an SQL query and return the result.
Definition: Database.php:1136
const NS_MAIN
Definition: Defines.php:64
getNonexistingTestPage( $title=null)
Returns a WikiPage representing a non-existing page.
$called
$called tracks whether the setUp and tearDown method has been called.
const CACHE_ACCEL
Definition: Defines.php:105
setIniSetting( $name, $value)
Set an ini setting for the duration of the test.
listTables( $prefix=null, $fname=__METHOD__)
List all tables on the database.
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
serialize()
static setLBFactoryTriggers(LBFactory $LBFactory, Config $config)
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException' returning false will NOT prevent logging $e
Definition: hooks.txt:2172
doStashMwGlobals( $globalKeys)
setUpSchema(IMaintainableDatabase $db)
Applies the schema overrides returned by getSchemaOverrides(), after undoing any previously applied s...
if(!isset( $args[0])) $lang
insert( $table, $a, $fname=__METHOD__, $options=[])
INSERT wrapper, inserts an array into a table.
wfRecursiveRemoveDir( $dir)
Remove a directory and all its content.
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. 'ContentSecurityPolicyDefaultSource':Modify the allowed CSP load sources. This affects all directives except for the script directive. If you want to add a script source, see ContentSecurityPolicyScriptSource hook. & $defaultSrc:Array of Content-Security-Policy allowed sources $policyConfig:Current configuration for the Content-Security-Policy header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyDirectives':Modify the content security policy directives. Use this only if ContentSecurityPolicyDefaultSource and ContentSecurityPolicyScriptSource do not meet your needs. & $directives:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyScriptSource':Modify the allowed CSP script sources. Note that you also have to use ContentSecurityPolicyDefaultSource if you want non-script sources to be loaded from whatever you add. & $scriptSrc:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header '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:1276
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
markTestSkippedIfDbType( $type)
Skip the test if using the specified database type.
assertArrayEquals(array $expected, array $actual, $ordered=false, $named=false)
Assert that two arrays are equal.
run(PHPUnit_Framework_TestResult $result=null)
$source
$value
copyTestData(IDatabase $source, IDatabase $target)
Copy test data from one database connection to another.
truncateTable( $tableName, IDatabase $db=null)
Empties the given table and resets any auto-increment counters.
undoSchemaOverrides(IMaintainableDatabase $db, $oldOverrides)
Undoes the specified schema overrides.
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 MediaWikiServices
Definition: injection.txt:23
static isTalk( $index)
Is the given namespace a talk namespace?
mergeMwGlobalArrayValue( $name, $values)
Merges the given values into a MW global array variable.
static resetIdByNameCache()
Reset the cache used in idFromName().
Definition: User.php:946
MediaWikiServices $localServices
The local service locator, created during setUp().
static destroySingleton()
Destroy the singleton instance.
$wgJobClasses
Maps jobs to their handlers; extensions can add to this to provide custom jobs.
LoggerFactory service provider that creates loggers implemented by Monolog.
Definition: MonologSpi.php:115
getNewTempFile()
Obtains a new temporary file name.
const DB_MASTER
Definition: defines.php:26
arrayWrap(array $elements)
Utility method taking an array of elements and wrapping each element in its own array.
this hook is for auditing only 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:989
stashMwGlobals( $globalKeys)
Stashes the global, will be restored in tearDown()
const CACHE_MEMCACHED
Definition: Defines.php:104
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 'ImportHandleUnknownUser':When a user doesn 't exist locally, this hook is called to give extensions an opportunity to auto-create it. If the auto-creation is successful, return false. $name:User name '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. '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 '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 since 1.28! 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:1993
$wgGroupPermissions
Permission keys given to users in each group.
restoreLoggers()
Restores loggers replaced by setLogger().
static listTables(IMaintainableDatabase $db)
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:47
getDefaultWikitextNS()
Returns the ID of a namespace that defaults to Wikitext.
static getImmutableTestUser( $groups=[])
Get a TestUser object that the caller may not modify.
static clear()
Clear the registry.
assertTypeOrValue( $type, $actual, $value=false, $message='')
Asserts that the provided variable is of the specified internal type or equals the $value argument...
resetNamespaces()
Must be called whenever namespaces are changed, e.g., $wgExtraNamespaces is altered.
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password 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:780
dropMockTables(IMaintainableDatabase $db, array $tables)
Drops the given mock tables.
setupAllTestDBs()
Set up all test DBs.
static getTestSysop()
Convenience method for getting an immutable admin test user.
const NS_PROJECT
Definition: Defines.php:68
listOriginalTables(IMaintainableDatabase $db, $prefix='prefixed')
Lists all tables in the live database schema.
static resetCache()
Reset the internal caching for unit testing.
wfTempDir()
Tries to get the system directory for temporary files.
static resetCache()
Reset the internal caching for unit testing.
static getMain()
Get the RequestContext object associated with the main request.
static prepareServices(Config $bootstrapConfig)
Interface for configuration instances.
Definition: Config.php:28
insertPage( $pageName, $text='Sample page for unit test.', $namespace=null, User $user=null)
Insert a new page.
setGroupPermissions( $newPerms, $newKey=null, $newValue=null)
Alters $wgGroupPermissions for the duration of the test.
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title e g db for database replication lag or jobqueue for job queue size converted to pseudo seconds It is possible to add more fields and they will be returned to the user in the API response 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:2220
editPage( $pageName, $text, $summary='', $defaultNs=NS_MAIN)
Edits or creates a page/revision.
revisionDelete( $rev, array $value=[Revision::DELETED_TEXT=> 1], $comment='')
Revision-deletes a revision.
static teardownTestDB()
Restores MediaWiki to using the table set (table prefix) it was using before setupTestDB() was called...
static getMutableTestUser( $groups=[])
Convenience method for getting a mutable test user.
$res
Definition: database.txt:21
static setupExternalStoreTestDBs( $testPrefix=null)
Clones the External Store database(s) for testing.
static createList( $typeName, IContextSource $context, Title $title, array $ids)
Instantiate the appropriate list class for a given list of IDs.
objectAssociativeSort(array &$array)
Does an associative sort that works for objects.
tableName( $name, $format='quoted')
Format a table name ready for use in constructing an SQL query.
Definition: Database.php:2327
static getExternalStoreDatabaseConnections()
Gets master database connections for all of the ExternalStoreDB stores configured in $wgDefaultExtern...
static clearCaches()
Clear internal caches.
Definition: MWNamespace.php:77
MediaWiki Logger MonologSpi
Definition: logger.txt:58
static isNotUnittest( $table)
const NS_CATEGORY
Definition: Defines.php:78
const EDIT_SUPPRESS_RC
Definition: Defines.php:155
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:1995
static resetMain()
Resets singleton returned by getMain().
static getMutableTestUser( $testName, $groups=[])
Get a TestUser object that the caller may modify.
unserialize( $serialized)
static getContentNamespaces()
Get a list of all namespace indices which are considered to contain content.
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:780
lastError()
Get a description of the last error.
LoggerFactory service provider that creates LegacyLogger instances.
Definition: LegacySpi.php:37
static makeTestConfig(Config $baseConfig=null, Config $customOverrides=null)
Create a config suitable for testing, based on a base config, default overrides, and custom overrides...
$buffer
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:935
static resetNonServiceCaches()
Resets some non-service singleton instances and other static caches.
const NS_FILE
Definition: Defines.php:70
wfGetCaller( $level=2)
Get the name of the function which called this function wfGetCaller( 1 ) is the function with the wfG...
$GLOBALS['IP']
assertFileContains( $fileName, $actualData, $createIfMissing=false, $msg='')
Check whether file contains given data.
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition: hooks.txt:1779
overrideMwServices(Config $configOverrides=null, array $services=[])
Stashes the global instance of MediaWikiServices, and installs a new one, allowing test cases to over...
Provides a fallback sequence for Config objects.
Definition: MultiConfig.php:28
static clearLog()
Clears internal log array and deprecation tracking.
Definition: MWDebug.php:135
setMwGlobals( $pairs, $value=null)
Sets a global, maintaining a stashed version of the previous global to be restored in tearDown...
namespace and then decline to actually register it & $namespaces
Definition: hooks.txt:935
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
Definition: distributors.txt:9
static isUsingExternalStoreDB()
Check whether ExternalStoreDB is being used.
tableExists( $table, $fname=__METHOD__)
Query whether a given table exists.
const NS_MEDIAWIKI
Definition: Defines.php:72
static canShallowCopy( $value)
Check if we can back up a value by performing a shallow copy.
hideDeprecated( $function)
Don&#39;t throw a warning if $function is deprecated and called later.
array $mwGlobals
Holds original values of MediaWiki configuration settings to be restored in tearDown().
tablePrefix( $prefix=null)
Get/set the table prefix.
Definition: Database.php:595
assertSelect( $table, $fields, $condition, array $expectedRows, array $options=[], array $join_conds=[])
Asserts that the given database query yields the rows given by $expectedRows.
static getStoreObject( $proto, array $params=[])
Get an external store object of the given type, with the given parameters.
const DELETED_TEXT
Definition: Revision.php:48
static TestUser [] $users
query( $sql, $fname=__METHOD__, $tempIgnore=false)
Run an SQL query and return the result.
static MediaWikiServices null $originalServices
The original service locator.
recloneMockTables(IMaintainableDatabase $db, array $tables)
Re-clones the given mock tables to restore them based on the live database schema.
setService( $name, $object)
Sets a service, maintaining a stashed version of the previous service to be restored in tearDown...
$wgDiff3
Path to the GNU diff3 utility.
int $phpErrorLevel
Original value of PHP&#39;s error_reporting setting.
static installMockMwServices(Config $configOverrides=null)
Creates a new "mock" MediaWikiServices instance, and installs it.
containsClosure( $var, $maxDepth=15)
checkPHPExtension( $extName)
Check if $extName is a loaded PHP extension, will skip the test whenever it is not loaded...
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:35
getType()
Get the type of the DBMS, as it appears in $wgDBtype.
MediaWiki Logger LegacySpi
Definition: logger.txt:53
const NS_HELP
Definition: Defines.php:76
array $cliArgs
The CLI arguments passed through from phpunit.php.
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
const EDIT_NEW
Definition: Defines.php:152
Relational database abstraction object.
Definition: Database.php:48
wfMkdirParents( $dir, $mode=null, $caller=null)
Make directory, and make all parent directories if they don&#39;t exist.
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
Definition: Database.php:1698
doSetMwGlobals( $pairs, $value=null)
An internal method that allows setService() to set globals that tests are not supposed to touch...
Advanced database interface for IDatabase handles that include maintenance methods.
markTestSkippedIfNoDiff3()
Check, if $wgDiff3 is set and ready to merge Will mark the calling test as skipped, if not ready.
array $iniSettings
Holds original values of ini settings to be restored in tearDown().
static getTestPrefixFor(IDatabase $db)
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
$wgDBprefix
Table name prefix; this should be alphanumeric and not contain spaces nor hyphens.
__construct( $name=null, array $data=[], $dataName='')
array $mwGlobalsToUnset
Holds list of MediaWiki configuration settings to be unset in tearDown().
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
$wgNamespaceContentModels
Associative array mapping namespace IDs to the name of the content model pages in that namespace shou...
useTemporaryTables( $u=true)
Set whether to use temporary tables or not.
makeConfig( $name)
Create a given Config using the registered callback for $name.
delete( $table, $conds, $fname=__METHOD__)
DELETE query wrapper.
Definition: Database.php:2939
static unprefixTable(&$tableName, $ind, $prefix)
static clearCaches()
Intended for tests that may change configuration in a way that invalidates caches.
Definition: Language.php:283
MediaWiki Logger LoggerFactory implements a PSR [0] compatible message logging system Named Psr Log LoggerInterface instances can be obtained from the MediaWiki Logger LoggerFactory::getInstance() static method. MediaWiki\Logger\LoggerFactory expects a class implementing the MediaWiki\Logger\Spi interface to act as a factory for new Psr\Log\LoggerInterface instances. The "Spi" in MediaWiki\Logger\Spi stands for "service provider interface". An SPI is an API intended to be implemented or extended by a third party. This software design pattern is intended to enable framework extension and replaceable components. It is specifically used in the MediaWiki\Logger\LoggerFactory service to allow alternate PSR-3 logging implementations to be easily integrated with MediaWiki. The service provider interface allows the backend logging library to be implemented in multiple ways. The $wgMWLoggerDefaultSpi global provides the classname of the default MediaWiki\Logger\Spi implementation to be loaded at runtime. This can either be the name of a class implementing the MediaWiki\Logger\Spi with a zero argument const ructor or a callable that will return an MediaWiki\Logger\Spi instance. Alternately the MediaWiki\Logger\LoggerFactory MediaWiki Logger LoggerFactory
Definition: logger.txt:5
Database $db
Primary database.
static singleton( $domain=false)
sourceFile( $filename, callable $lineCallback=null, callable $resultCallback=null, $fname=false, callable $inputCallback=null)
Read and execute SQL commands from a file.
getNewTempDirectory()
obtains a new temporary directory
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:271
if(! $wgDBerrorLogTZ) $wgRequest
Definition: Setup.php:746
static wfResetOutputBuffersBarrier( $buffer)
Used as a marker to prevent wfResetOutputBuffers from breaking PHPUnit.
static makeTestConfigFactoryInstantiator(ConfigFactory $oldFactory, array $configurations)
const DB_REPLICA
Definition: defines.php:25
array $tmpFiles
Holds the paths of temporary files/directories created through getNewTempFile, and getNewTempDirector...
static clear()
Clear all the cached instances.
assertHTMLEquals( $expected, $actual, $msg='')
Put each HTML element on its own line and then equals() the results.
static setupTestDB(Database $db, $prefix)
Creates an empty skeleton of the wiki database by cloning its structure to equivalent tables using th...
getExistingTestPage( $title=null)
Returns a WikiPage representing an existing page.
const CACHE_NONE
Definition: Defines.php:102
$wgSQLMode
SQL Mode - default is turning off all modes, including strict, if set.
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:120
static getSchemaOverrides(IMaintainableDatabase $db)
Stub.
static makeContent( $text, Title $title=null, $modelId=null, $format=null)
Convenience function for creating a Content object from a given textual representation.
A Config instance which stores all settings as a member variable.
Definition: HashConfig.php:28
static getValidNamespaces()
Returns an array of the namespaces (by integer id) that exist on the wiki.
MediaWikiServices is the service locator for the application scope of MediaWiki.
resetDB( $db, $tablesUsed)
Empty all tables so they can be repopulated for tests.
tablePrefix( $prefix=null)
Get/set the table prefix.
setLogger( $channel, LoggerInterface $logger)
Sets the logger for a specified channel, for the duration of the test.
static getTestUser( $groups=[])
Convenience method for getting an immutable test user.
static changePrefix( $prefix)
Change the table prefix on all open DB connections/.
static stripStringKeys(&$r)
Utility function for eliminating all string keys from an array.
tableName( $name, $format='quoted')
Format a table name ready for use in constructing an SQL query.
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1486
const CACHE_DB
Definition: Defines.php:103
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
testMediaWikiTestCaseParentSetupCalled()
Make sure MediaWikiTestCase extending classes have called their parent setUp method.
static restoreMwServices()
Restores the original, non-mock MediaWikiServices instance.
static destroySingletons()
Destroy the singleton instances.
isWikitextNS( $ns)
Returns true if the given namespace defaults to Wikitext according to $wgNamespaceContentModels.
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:280