MediaWiki  master
MediaWikiTestCase.php
Go to the documentation of this file.
1 <?php
2 
13 
18 
21 
27  private static $originalServices;
28 
33  private $localServices;
34 
47  private $called = [];
48 
53  public static $users;
54 
61  protected $db;
62 
67  protected $tablesUsed = []; // tables with data
68 
69  private static $useTemporaryTables = true;
70  private static $reuseDB = false;
71  private static $dbSetup = false;
72  private static $oldTablePrefix = '';
73 
79  private $phpErrorLevel;
80 
87  private $tmpFiles = [];
88 
95  private $mwGlobals = [];
96 
102  private $mwGlobalsToUnset = [];
103 
110  private $iniSettings = [];
111 
116  private $loggers = [];
117 
122  private $cliArgs = [];
123 
129  private $overriddenServices = [];
130 
134  const DB_PREFIX = 'unittest_';
135  const ORA_DB_PREFIX = 'ut_';
136 
141  protected $supportedDBs = [
142  'mysql',
143  'sqlite',
144  'postgres',
145  'oracle'
146  ];
147 
148  public function __construct( $name = null, array $data = [], $dataName = '' ) {
149  parent::__construct( $name, $data, $dataName );
150 
151  $this->backupGlobals = false;
152  $this->backupStaticAttributes = false;
153  }
154 
155  public function __destruct() {
156  // Complain if self::setUp() was called, but not self::tearDown()
157  // $this->called['setUp'] will be checked by self::testMediaWikiTestCaseParentSetupCalled()
158  if ( isset( $this->called['setUp'] ) && !isset( $this->called['tearDown'] ) ) {
159  throw new MWException( static::class . "::tearDown() must call parent::tearDown()" );
160  }
161  }
162 
163  public static function setUpBeforeClass() {
164  parent::setUpBeforeClass();
165 
166  // Get the original service locator
167  if ( !self::$originalServices ) {
168  self::$originalServices = MediaWikiServices::getInstance();
169  }
170  }
171 
180  public static function getTestUser( $groups = [] ) {
181  return TestUserRegistry::getImmutableTestUser( $groups );
182  }
183 
192  public static function getMutableTestUser( $groups = [] ) {
193  return TestUserRegistry::getMutableTestUser( __CLASS__, $groups );
194  }
195 
204  public static function getTestSysop() {
205  return self::getTestUser( [ 'sysop', 'bureaucrat' ] );
206  }
207 
220  protected function getExistingTestPage( $title = null ) {
221  if ( !$this->needsDB() ) {
222  throw new MWException( 'When testing which pages, the test cases\'s needsDB()' .
223  ' method should return true. Use @group Database or $this->tablesUsed.' );
224  }
225 
226  $title = ( $title === null ) ? 'UTPage' : $title;
227  $title = is_string( $title ) ? Title::newFromText( $title ) : $title;
228  $page = WikiPage::factory( $title );
229 
230  if ( !$page->exists() ) {
231  $user = self::getTestSysop()->getUser();
232  $page->doEditContent(
233  new WikitextContent( 'UTContent' ),
234  'UTPageSummary',
236  false,
237  $user
238  );
239  }
240 
241  return $page;
242  }
243 
256  protected function getNonexistingTestPage( $title = null ) {
257  if ( !$this->needsDB() ) {
258  throw new MWException( 'When testing which pages, the test cases\'s needsDB()' .
259  ' method should return true. Use @group Database or $this->tablesUsed.' );
260  }
261 
262  $title = ( $title === null ) ? 'UTPage-' . rand( 0, 100000 ) : $title;
263  $title = is_string( $title ) ? Title::newFromText( $title ) : $title;
264  $page = WikiPage::factory( $title );
265 
266  if ( $page->exists() ) {
267  $page->doDeleteArticle( 'Testing' );
268  }
269 
270  return $page;
271  }
272 
276  public static function prepareServices( Config $bootstrapConfig ) {
277  }
278 
288  private static function makeTestConfig(
289  Config $baseConfig = null,
290  Config $customOverrides = null
291  ) {
292  $defaultOverrides = new HashConfig();
293 
294  if ( !$baseConfig ) {
295  $baseConfig = self::$originalServices->getBootstrapConfig();
296  }
297 
298  /* Some functions require some kind of caching, and will end up using the db,
299  * which we can't allow, as that would open a new connection for mysql.
300  * Replace with a HashBag. They would not be going to persist anyway.
301  */
302  $hashCache = [ 'class' => HashBagOStuff::class, 'reportDupes' => false ];
303  $objectCaches = [
304  CACHE_DB => $hashCache,
305  CACHE_ACCEL => $hashCache,
306  CACHE_MEMCACHED => $hashCache,
307  'apc' => $hashCache,
308  'apcu' => $hashCache,
309  'wincache' => $hashCache,
310  ] + $baseConfig->get( 'ObjectCaches' );
311 
312  $defaultOverrides->set( 'ObjectCaches', $objectCaches );
313  $defaultOverrides->set( 'MainCacheType', CACHE_NONE );
314  $defaultOverrides->set( 'JobTypeConf', [ 'default' => [ 'class' => JobQueueMemory::class ] ] );
315 
316  // Use a fast hash algorithm to hash passwords.
317  $defaultOverrides->set( 'PasswordDefault', 'A' );
318 
319  $testConfig = $customOverrides
320  ? new MultiConfig( [ $customOverrides, $defaultOverrides, $baseConfig ] )
321  : new MultiConfig( [ $defaultOverrides, $baseConfig ] );
322 
323  return $testConfig;
324  }
325 
332  private static function makeTestConfigFactoryInstantiator(
333  ConfigFactory $oldFactory,
334  array $configurations
335  ) {
336  return function ( MediaWikiServices $services ) use ( $oldFactory, $configurations ) {
337  $factory = new ConfigFactory();
338 
339  // clone configurations from $oldFactory that are not overwritten by $configurations
340  $namesToClone = array_diff(
341  $oldFactory->getConfigNames(),
342  array_keys( $configurations )
343  );
344 
345  foreach ( $namesToClone as $name ) {
346  $factory->register( $name, $oldFactory->makeConfig( $name ) );
347  }
348 
349  foreach ( $configurations as $name => $config ) {
350  $factory->register( $name, $config );
351  }
352 
353  return $factory;
354  };
355  }
356 
361  public static function resetNonServiceCaches() {
362  global $wgRequest, $wgJobClasses;
363 
364  foreach ( $wgJobClasses as $type => $class ) {
365  JobQueueGroup::singleton()->get( $type )->delete();
366  }
368 
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();
382  }
383 
384  public function run( PHPUnit_Framework_TestResult $result = null ) {
385  if ( $result instanceof MediaWikiTestResult ) {
386  $this->cliArgs = $result->getMediaWikiCliArgs();
387  }
388  $this->overrideMwServices();
389 
390  if ( $this->needsDB() && !$this->isTestInDatabaseGroup() ) {
391  throw new Exception(
392  get_class( $this ) . ' apparently needsDB but is not in the Database group'
393  );
394  }
395 
396  $needsResetDB = false;
397  if ( !self::$dbSetup || $this->needsDB() ) {
398  // set up a DB connection for this test to use
399 
400  self::$useTemporaryTables = !$this->getCliArg( 'use-normal-tables' );
401  self::$reuseDB = $this->getCliArg( 'reuse-db' );
402 
403  $this->db = wfGetDB( DB_MASTER );
404 
405  $this->checkDbIsSupported();
406 
407  if ( !self::$dbSetup ) {
408  $this->setupAllTestDBs();
409  $this->addCoreDBData();
410  }
411 
412  // TODO: the DB setup should be done in setUpBeforeClass(), so the test DB
413  // is available in subclass's setUpBeforeClass() and setUp() methods.
414  // This would also remove the need for the HACK that is oncePerClass().
415  if ( $this->oncePerClass() ) {
416  $this->setUpSchema( $this->db );
417  $this->resetDB( $this->db, $this->tablesUsed );
418  $this->addDBDataOnce();
419  }
420 
421  $this->addDBData();
422  $needsResetDB = true;
423  }
424 
425  parent::run( $result );
426 
427  // We don't mind if we override already-overridden services during cleanup
428  $this->overriddenServices = [];
429 
430  if ( $needsResetDB ) {
431  $this->resetDB( $this->db, $this->tablesUsed );
432  }
433 
434  self::restoreMwServices();
435  $this->localServices = null;
436  }
437 
441  private function oncePerClass() {
442  // Remember current test class in the database connection,
443  // so we know when we need to run addData.
444 
445  $class = static::class;
446 
447  $first = !isset( $this->db->_hasDataForTestClass )
448  || $this->db->_hasDataForTestClass !== $class;
449 
450  $this->db->_hasDataForTestClass = $class;
451  return $first;
452  }
453 
459  public function usesTemporaryTables() {
460  return self::$useTemporaryTables;
461  }
462 
472  protected function getNewTempFile() {
473  $fileName = tempnam( wfTempDir(), 'MW_PHPUnit_' . static::class . '_' );
474  $this->tmpFiles[] = $fileName;
475 
476  return $fileName;
477  }
478 
489  protected function getNewTempDirectory() {
490  // Starting of with a temporary /file/.
491  $fileName = $this->getNewTempFile();
492 
493  // Converting the temporary /file/ to a /directory/
494  // The following is not atomic, but at least we now have a single place,
495  // where temporary directory creation is bundled and can be improved
496  unlink( $fileName );
497  $this->assertTrue( wfMkdirParents( $fileName ) );
498 
499  return $fileName;
500  }
501 
502  protected function setUp() {
503  parent::setUp();
504  $this->called['setUp'] = true;
505 
506  $this->phpErrorLevel = intval( ini_get( 'error_reporting' ) );
507 
508  $this->overriddenServices = [];
509 
510  // Cleaning up temporary files
511  foreach ( $this->tmpFiles as $fileName ) {
512  if ( is_file( $fileName ) || ( is_link( $fileName ) ) ) {
513  unlink( $fileName );
514  } elseif ( is_dir( $fileName ) ) {
515  wfRecursiveRemoveDir( $fileName );
516  }
517  }
518 
519  if ( $this->needsDB() && $this->db ) {
520  // Clean up open transactions
521  while ( $this->db->trxLevel() > 0 ) {
522  $this->db->rollback( __METHOD__, 'flush' );
523  }
524  // Check for unsafe queries
525  if ( $this->db->getType() === 'mysql' ) {
526  $this->db->query( "SET sql_mode = 'STRICT_ALL_TABLES'", __METHOD__ );
527  }
528  }
529 
530  // Reset all caches between tests.
531  self::resetNonServiceCaches();
532 
533  // XXX: reset maintenance triggers
534  // Hook into period lag checks which often happen in long-running scripts
535  $lbFactory = $this->localServices->getDBLoadBalancerFactory();
536  Maintenance::setLBFactoryTriggers( $lbFactory, $this->localServices->getMainConfig() );
537 
538  ob_start( 'MediaWikiTestCase::wfResetOutputBuffersBarrier' );
539  }
540 
541  protected function addTmpFiles( $files ) {
542  $this->tmpFiles = array_merge( $this->tmpFiles, (array)$files );
543  }
544 
545  protected function tearDown() {
546  global $wgRequest, $wgSQLMode;
547 
548  $status = ob_get_status();
549  if ( isset( $status['name'] ) &&
550  $status['name'] === 'MediaWikiTestCase::wfResetOutputBuffersBarrier'
551  ) {
552  ob_end_flush();
553  }
554 
555  $this->called['tearDown'] = true;
556  // Cleaning up temporary files
557  foreach ( $this->tmpFiles as $fileName ) {
558  if ( is_file( $fileName ) || ( is_link( $fileName ) ) ) {
559  unlink( $fileName );
560  } elseif ( is_dir( $fileName ) ) {
561  wfRecursiveRemoveDir( $fileName );
562  }
563  }
564 
565  if ( $this->needsDB() && $this->db ) {
566  // Clean up open transactions
567  while ( $this->db->trxLevel() > 0 ) {
568  $this->db->rollback( __METHOD__, 'flush' );
569  }
570  if ( $this->db->getType() === 'mysql' ) {
571  $this->db->query( "SET sql_mode = " . $this->db->addQuotes( $wgSQLMode ),
572  __METHOD__ );
573  }
574  }
575 
576  // Re-enable any disabled deprecation warnings
578  // Restore mw globals
579  foreach ( $this->mwGlobals as $key => $value ) {
580  $GLOBALS[$key] = $value;
581  }
582  foreach ( $this->mwGlobalsToUnset as $value ) {
583  unset( $GLOBALS[$value] );
584  }
585  foreach ( $this->iniSettings as $name => $value ) {
586  ini_set( $name, $value );
587  }
588  if (
589  array_key_exists( 'wgExtraNamespaces', $this->mwGlobals ) ||
590  in_array( 'wgExtraNamespaces', $this->mwGlobalsToUnset )
591  ) {
592  $this->resetNamespaces();
593  }
594  $this->mwGlobals = [];
595  $this->mwGlobalsToUnset = [];
596  $this->restoreLoggers();
597 
598  // TODO: move global state into MediaWikiServices
600  if ( session_id() !== '' ) {
601  session_write_close();
602  session_id( '' );
603  }
604  $wgRequest = new FauxRequest();
607 
608  $phpErrorLevel = intval( ini_get( 'error_reporting' ) );
609 
610  if ( $phpErrorLevel !== $this->phpErrorLevel ) {
611  ini_set( 'error_reporting', $this->phpErrorLevel );
612 
613  $oldHex = strtoupper( dechex( $this->phpErrorLevel ) );
614  $newHex = strtoupper( dechex( $phpErrorLevel ) );
615  $message = "PHP error_reporting setting was left dirty: "
616  . "was 0x$oldHex before test, 0x$newHex after test!";
617 
618  $this->fail( $message );
619  }
620 
621  parent::tearDown();
622  }
623 
632  final public function testMediaWikiTestCaseParentSetupCalled() {
633  $this->assertArrayHasKey( 'setUp', $this->called,
634  static::class . '::setUp() must call parent::setUp()'
635  );
636  }
637 
647  protected function setService( $name, $object ) {
648  if ( !$this->localServices ) {
649  throw new Exception( __METHOD__ . ' must be called after MediaWikiTestCase::run()' );
650  }
651 
652  if ( $this->localServices !== MediaWikiServices::getInstance() ) {
653  throw new Exception( __METHOD__ . ' will not work because the global MediaWikiServices '
654  . 'instance has been replaced by test code.' );
655  }
656 
657  $this->overriddenServices[] = $name;
658 
659  $this->localServices->disableService( $name );
660  $this->localServices->redefineService(
661  $name,
662  function () use ( $object ) {
663  return $object;
664  }
665  );
666 
667  if ( $name === 'ContentLanguage' ) {
668  $this->doSetMwGlobals( [ 'wgContLang' => $object ] );
669  }
670  }
671 
707  protected function setMwGlobals( $pairs, $value = null ) {
708  if ( is_string( $pairs ) ) {
709  $pairs = [ $pairs => $value ];
710  }
711 
712  if ( isset( $pairs['wgContLang'] ) ) {
713  throw new MWException(
714  'No setting $wgContLang, use setContentLang() or setService( \'ContentLanguage\' )'
715  );
716  }
717 
718  $this->doSetMwGlobals( $pairs, $value );
719  }
720 
725  private function doSetMwGlobals( $pairs, $value = null ) {
726  $this->doStashMwGlobals( array_keys( $pairs ) );
727 
728  foreach ( $pairs as $key => $value ) {
729  $GLOBALS[$key] = $value;
730  }
731 
732  if ( array_key_exists( 'wgExtraNamespaces', $pairs ) ) {
733  $this->resetNamespaces();
734  }
735  }
736 
743  protected function setIniSetting( $name, $value ) {
744  $original = ini_get( $name );
745  $this->iniSettings[$name] = $original;
746  ini_set( $name, $value );
747  }
748 
753  private function resetNamespaces() {
754  if ( !$this->localServices ) {
755  throw new Exception( __METHOD__ . ' must be called after MediaWikiTestCase::run()' );
756  }
757 
758  if ( $this->localServices !== MediaWikiServices::getInstance() ) {
759  throw new Exception( __METHOD__ . ' will not work because the global MediaWikiServices '
760  . 'instance has been replaced by test code.' );
761  }
762 
765 
766  // We can't have the TitleFormatter holding on to an old Language object either
767  // @todo We shouldn't need to reset all the aliases here.
768  $this->localServices->resetServiceForTesting( 'TitleFormatter' );
769  $this->localServices->resetServiceForTesting( 'TitleParser' );
770  $this->localServices->resetServiceForTesting( '_MediaWikiTitleCodec' );
771  }
772 
781  private static function canShallowCopy( $value ) {
782  if ( is_scalar( $value ) || $value === null ) {
783  return true;
784  }
785  if ( is_array( $value ) ) {
786  foreach ( $value as $subValue ) {
787  if ( !is_scalar( $subValue ) && $subValue !== null ) {
788  return false;
789  }
790  }
791  return true;
792  }
793  return false;
794  }
795 
814  protected function stashMwGlobals( $globalKeys ) {
815  wfDeprecated( __METHOD__, '1.32' );
816  $this->doStashMwGlobals( $globalKeys );
817  }
818 
819  private function doStashMwGlobals( $globalKeys ) {
820  if ( is_string( $globalKeys ) ) {
821  $globalKeys = [ $globalKeys ];
822  }
823 
824  foreach ( $globalKeys as $globalKey ) {
825  // NOTE: make sure we only save the global once or a second call to
826  // setMwGlobals() on the same global would override the original
827  // value.
828  if (
829  !array_key_exists( $globalKey, $this->mwGlobals ) &&
830  !array_key_exists( $globalKey, $this->mwGlobalsToUnset )
831  ) {
832  if ( !array_key_exists( $globalKey, $GLOBALS ) ) {
833  $this->mwGlobalsToUnset[$globalKey] = $globalKey;
834  continue;
835  }
836  // NOTE: we serialize then unserialize the value in case it is an object
837  // this stops any objects being passed by reference. We could use clone
838  // and if is_object but this does account for objects within objects!
839  if ( self::canShallowCopy( $GLOBALS[$globalKey] ) ) {
840  $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
841  } elseif (
842  // Many MediaWiki types are safe to clone. These are the
843  // ones that are most commonly stashed.
844  $GLOBALS[$globalKey] instanceof Language ||
845  $GLOBALS[$globalKey] instanceof User ||
846  $GLOBALS[$globalKey] instanceof FauxRequest
847  ) {
848  $this->mwGlobals[$globalKey] = clone $GLOBALS[$globalKey];
849  } elseif ( $this->containsClosure( $GLOBALS[$globalKey] ) ) {
850  // Serializing Closure only gives a warning on HHVM while
851  // it throws an Exception on Zend.
852  // Workaround for https://github.com/facebook/hhvm/issues/6206
853  $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
854  } else {
855  try {
856  $this->mwGlobals[$globalKey] = unserialize( serialize( $GLOBALS[$globalKey] ) );
857  } catch ( Exception $e ) {
858  $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
859  }
860  }
861  }
862  }
863  }
864 
871  private function containsClosure( $var, $maxDepth = 15 ) {
872  if ( $var instanceof Closure ) {
873  return true;
874  }
875  if ( !is_array( $var ) || $maxDepth === 0 ) {
876  return false;
877  }
878 
879  foreach ( $var as $value ) {
880  if ( $this->containsClosure( $value, $maxDepth - 1 ) ) {
881  return true;
882  }
883  }
884  return false;
885  }
886 
902  protected function mergeMwGlobalArrayValue( $name, $values ) {
903  if ( !isset( $GLOBALS[$name] ) ) {
904  $merged = $values;
905  } else {
906  if ( !is_array( $GLOBALS[$name] ) ) {
907  throw new MWException( "MW global $name is not an array." );
908  }
909 
910  // NOTE: do not use array_merge, it screws up for numeric keys.
911  $merged = $GLOBALS[$name];
912  foreach ( $values as $k => $v ) {
913  $merged[$k] = $v;
914  }
915  }
916 
917  $this->setMwGlobals( $name, $merged );
918  }
919 
935  protected function overrideMwServices(
936  Config $configOverrides = null, array $services = []
937  ) {
938  if ( $this->overriddenServices ) {
939  throw new MWException(
940  'The following services were set and are now being unset by overrideMwServices: ' .
941  implode( ', ', $this->overriddenServices )
942  );
943  }
944  $newInstance = self::installMockMwServices( $configOverrides );
945 
946  if ( $this->localServices ) {
947  $this->localServices->destroy();
948  }
949 
950  $this->localServices = $newInstance;
951 
952  foreach ( $services as $name => $callback ) {
953  $newInstance->redefineService( $name, $callback );
954  }
955 
956  return $newInstance;
957  }
958 
976  public static function installMockMwServices( Config $configOverrides = null ) {
977  // Make sure we have the original service locator
978  if ( !self::$originalServices ) {
979  self::$originalServices = MediaWikiServices::getInstance();
980  }
981 
982  if ( !$configOverrides ) {
983  $configOverrides = new HashConfig();
984  }
985 
986  $oldConfigFactory = self::$originalServices->getConfigFactory();
987  $oldLoadBalancerFactory = self::$originalServices->getDBLoadBalancerFactory();
988 
989  $testConfig = self::makeTestConfig( null, $configOverrides );
990  $newServices = new MediaWikiServices( $testConfig );
991 
992  // Load the default wiring from the specified files.
993  // NOTE: this logic mirrors the logic in MediaWikiServices::newInstance.
994  $wiringFiles = $testConfig->get( 'ServiceWiringFiles' );
995  $newServices->loadWiringFiles( $wiringFiles );
996 
997  // Provide a traditional hook point to allow extensions to configure services.
998  Hooks::run( 'MediaWikiServices', [ $newServices ] );
999 
1000  // Use bootstrap config for all configuration.
1001  // This allows config overrides via global variables to take effect.
1002  $bootstrapConfig = $newServices->getBootstrapConfig();
1003  $newServices->resetServiceForTesting( 'ConfigFactory' );
1004  $newServices->redefineService(
1005  'ConfigFactory',
1006  self::makeTestConfigFactoryInstantiator(
1007  $oldConfigFactory,
1008  [ 'main' => $bootstrapConfig ]
1009  )
1010  );
1011  $newServices->resetServiceForTesting( 'DBLoadBalancerFactory' );
1012  $newServices->redefineService(
1013  'DBLoadBalancerFactory',
1014  function ( MediaWikiServices $services ) use ( $oldLoadBalancerFactory ) {
1015  return $oldLoadBalancerFactory;
1016  }
1017  );
1018 
1019  MediaWikiServices::forceGlobalInstance( $newServices );
1020  return $newServices;
1021  }
1022 
1034  public static function restoreMwServices() {
1035  if ( !self::$originalServices ) {
1036  return false;
1037  }
1038 
1039  $currentServices = MediaWikiServices::getInstance();
1040 
1041  if ( self::$originalServices === $currentServices ) {
1042  return false;
1043  }
1044 
1045  MediaWikiServices::forceGlobalInstance( self::$originalServices );
1046  $currentServices->destroy();
1047 
1048  return true;
1049  }
1050 
1055  public function setUserLang( $lang ) {
1056  RequestContext::getMain()->setLanguage( $lang );
1057  $this->setMwGlobals( 'wgLang', RequestContext::getMain()->getLanguage() );
1058  }
1059 
1064  public function setContentLang( $lang ) {
1065  if ( $lang instanceof Language ) {
1066  $this->setMwGlobals( 'wgLanguageCode', $lang->getCode() );
1067  // Set to the exact object requested
1068  $this->setService( 'ContentLanguage', $lang );
1069  } else {
1070  $this->setMwGlobals( 'wgLanguageCode', $lang );
1071  // Let the service handler make up the object. Avoid calling setService(), because if
1072  // we do, overrideMwServices() will complain if it's called later on.
1073  $services = MediaWikiServices::getInstance();
1074  $services->resetServiceForTesting( 'ContentLanguage' );
1075  $this->doSetMwGlobals( [ 'wgContLang' => $services->getContentLanguage() ] );
1076  }
1077  }
1078 
1093  public function setGroupPermissions( $newPerms, $newKey = null, $newValue = null ) {
1094  global $wgGroupPermissions;
1095 
1096  if ( is_string( $newPerms ) ) {
1097  $newPerms = [ $newPerms => [ $newKey => $newValue ] ];
1098  }
1099 
1100  $newPermissions = $wgGroupPermissions;
1101  foreach ( $newPerms as $group => $permissions ) {
1102  foreach ( $permissions as $key => $value ) {
1103  $newPermissions[$group][$key] = $value;
1104  }
1105  }
1106 
1107  $this->setMwGlobals( 'wgGroupPermissions', $newPermissions );
1108  }
1109 
1116  protected function setLogger( $channel, LoggerInterface $logger ) {
1117  // TODO: Once loggers are managed by MediaWikiServices, use
1118  // overrideMwServices() to set loggers.
1119 
1120  $provider = LoggerFactory::getProvider();
1121  $wrappedProvider = TestingAccessWrapper::newFromObject( $provider );
1122  $singletons = $wrappedProvider->singletons;
1123  if ( $provider instanceof MonologSpi ) {
1124  if ( !isset( $this->loggers[$channel] ) ) {
1125  $this->loggers[$channel] = $singletons['loggers'][$channel] ?? null;
1126  }
1127  $singletons['loggers'][$channel] = $logger;
1128  } elseif ( $provider instanceof LegacySpi || $provider instanceof LogCapturingSpi ) {
1129  if ( !isset( $this->loggers[$channel] ) ) {
1130  $this->loggers[$channel] = $singletons[$channel] ?? null;
1131  }
1132  $singletons[$channel] = $logger;
1133  } else {
1134  throw new LogicException( __METHOD__ . ': setting a logger for ' . get_class( $provider )
1135  . ' is not implemented' );
1136  }
1137  $wrappedProvider->singletons = $singletons;
1138  }
1139 
1144  private function restoreLoggers() {
1145  $provider = LoggerFactory::getProvider();
1146  $wrappedProvider = TestingAccessWrapper::newFromObject( $provider );
1147  $singletons = $wrappedProvider->singletons;
1148  foreach ( $this->loggers as $channel => $logger ) {
1149  if ( $provider instanceof MonologSpi ) {
1150  if ( $logger === null ) {
1151  unset( $singletons['loggers'][$channel] );
1152  } else {
1153  $singletons['loggers'][$channel] = $logger;
1154  }
1155  } elseif ( $provider instanceof LegacySpi || $provider instanceof LogCapturingSpi ) {
1156  if ( $logger === null ) {
1157  unset( $singletons[$channel] );
1158  } else {
1159  $singletons[$channel] = $logger;
1160  }
1161  }
1162  }
1163  $wrappedProvider->singletons = $singletons;
1164  $this->loggers = [];
1165  }
1166 
1171  public function dbPrefix() {
1172  return self::getTestPrefixFor( $this->db );
1173  }
1174 
1180  public static function getTestPrefixFor( IDatabase $db ) {
1181  return $db->getType() == 'oracle' ? self::ORA_DB_PREFIX : self::DB_PREFIX;
1182  }
1183 
1188  public function needsDB() {
1189  // If the test says it uses database tables, it needs the database
1190  return $this->tablesUsed || $this->isTestInDatabaseGroup();
1191  }
1192 
1197  protected function isTestInDatabaseGroup() {
1198  // If the test class says it belongs to the Database group, it needs the database.
1199  // NOTE: This ONLY checks for the group in the class level doc comment.
1200  $rc = new ReflectionClass( $this );
1201  return (bool)preg_match( '/@group +Database/im', $rc->getDocComment() );
1202  }
1203 
1220  protected function insertPage(
1221  $pageName,
1222  $text = 'Sample page for unit test.',
1223  $namespace = null,
1224  User $user = null
1225  ) {
1226  if ( !$this->needsDB() ) {
1227  throw new MWException( 'When testing which pages, the test cases\'s needsDB()' .
1228  ' method should return true. Use @group Database or $this->tablesUsed.' );
1229  }
1230 
1231  if ( is_string( $pageName ) ) {
1232  $title = Title::newFromText( $pageName, $namespace );
1233  } else {
1234  $title = $pageName;
1235  }
1236 
1237  if ( !$user ) {
1238  $user = static::getTestSysop()->getUser();
1239  }
1240  $comment = __METHOD__ . ': Sample page for unit test.';
1241 
1242  $page = WikiPage::factory( $title );
1243  $page->doEditContent( ContentHandler::makeContent( $text, $title ), $comment, 0, false, $user );
1244 
1245  return [
1246  'title' => $title,
1247  'id' => $page->getId(),
1248  ];
1249  }
1250 
1266  public function addDBDataOnce() {
1267  }
1268 
1278  public function addDBData() {
1279  }
1280 
1284  protected function addCoreDBData() {
1285  if ( $this->db->getType() == 'oracle' ) {
1286  # Insert 0 user to prevent FK violations
1287  # Anonymous user
1288  if ( !$this->db->selectField( 'user', '1', [ 'user_id' => 0 ] ) ) {
1289  $this->db->insert( 'user', [
1290  'user_id' => 0,
1291  'user_name' => 'Anonymous' ], __METHOD__, [ 'IGNORE' ] );
1292  }
1293 
1294  # Insert 0 page to prevent FK violations
1295  # Blank page
1296  if ( !$this->db->selectField( 'page', '1', [ 'page_id' => 0 ] ) ) {
1297  $this->db->insert( 'page', [
1298  'page_id' => 0,
1299  'page_namespace' => 0,
1300  'page_title' => ' ',
1301  'page_restrictions' => null,
1302  'page_is_redirect' => 0,
1303  'page_is_new' => 0,
1304  'page_random' => 0,
1305  'page_touched' => $this->db->timestamp(),
1306  'page_latest' => 0,
1307  'page_len' => 0 ], __METHOD__, [ 'IGNORE' ] );
1308  }
1309  }
1310 
1312 
1314 
1315  // Make sysop user
1316  $user = static::getTestSysop()->getUser();
1317 
1318  // Make 1 page with 1 revision
1319  $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
1320  if ( $page->getId() == 0 ) {
1321  $page->doEditContent(
1322  new WikitextContent( 'UTContent' ),
1323  'UTPageSummary',
1325  false,
1326  $user
1327  );
1328  // an edit always attempt to purge backlink links such as history
1329  // pages. That is unnecessary.
1330  JobQueueGroup::singleton()->get( 'htmlCacheUpdate' )->delete();
1331  // WikiPages::doEditUpdates randomly adds RC purges
1332  JobQueueGroup::singleton()->get( 'recentChangesUpdate' )->delete();
1333 
1334  // doEditContent() probably started the session via
1335  // User::loadFromSession(). Close it now.
1336  if ( session_id() !== '' ) {
1337  session_write_close();
1338  session_id( '' );
1339  }
1340  }
1341  }
1342 
1352  public static function teardownTestDB() {
1353  global $wgJobClasses;
1354 
1355  if ( !self::$dbSetup ) {
1356  return;
1357  }
1358 
1359  Hooks::run( 'UnitTestsBeforeDatabaseTeardown' );
1360 
1361  foreach ( $wgJobClasses as $type => $class ) {
1362  // Delete any jobs under the clone DB (or old prefix in other stores)
1363  JobQueueGroup::singleton()->get( $type )->delete();
1364  }
1365 
1366  CloneDatabase::changePrefix( self::$oldTablePrefix );
1367 
1368  self::$oldTablePrefix = false;
1369  self::$dbSetup = false;
1370  }
1371 
1383  protected static function setupDatabaseWithTestPrefix(
1385  $prefix = null
1386  ) {
1387  if ( $prefix === null ) {
1388  $prefix = self::getTestPrefixFor( $db );
1389  }
1390 
1391  if ( ( $db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
1392  $db->tablePrefix( $prefix );
1393  return false;
1394  }
1395 
1396  if ( !isset( $db->_originalTablePrefix ) ) {
1397  $oldPrefix = $db->tablePrefix();
1398 
1399  if ( $oldPrefix === $prefix ) {
1400  // table already has the correct prefix, but presumably no cloned tables
1401  $oldPrefix = self::$oldTablePrefix;
1402  }
1403 
1404  $db->tablePrefix( $oldPrefix );
1405  $tablesCloned = self::listTables( $db );
1406  $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix, $oldPrefix );
1407  $dbClone->useTemporaryTables( self::$useTemporaryTables );
1408 
1409  $dbClone->cloneTableStructure();
1410 
1411  $db->tablePrefix( $prefix );
1412  $db->_originalTablePrefix = $oldPrefix;
1413  }
1414 
1415  return true;
1416  }
1417 
1421  public function setupAllTestDBs() {
1422  global $wgDBprefix;
1423 
1424  self::$oldTablePrefix = $wgDBprefix;
1425 
1426  $testPrefix = $this->dbPrefix();
1427 
1428  // switch to a temporary clone of the database
1429  self::setupTestDB( $this->db, $testPrefix );
1430 
1431  if ( self::isUsingExternalStoreDB() ) {
1432  self::setupExternalStoreTestDBs( $testPrefix );
1433  }
1434 
1435  // NOTE: Change the prefix in the LBFactory and $wgDBprefix, to prevent
1436  // *any* database connections to operate on live data.
1437  CloneDatabase::changePrefix( $testPrefix );
1438  }
1439 
1461  public static function setupTestDB( Database $db, $prefix ) {
1462  if ( self::$dbSetup ) {
1463  return;
1464  }
1465 
1466  if ( $db->tablePrefix() === $prefix ) {
1467  throw new MWException(
1468  'Cannot run unit tests, the database prefix is already "' . $prefix . '"' );
1469  }
1470 
1471  // TODO: the below should be re-written as soon as LBFactory, LoadBalancer,
1472  // and Database no longer use global state.
1473 
1474  self::$dbSetup = true;
1475 
1476  if ( !self::setupDatabaseWithTestPrefix( $db, $prefix ) ) {
1477  return;
1478  }
1479 
1480  // Assuming this isn't needed for External Store database, and not sure if the procedure
1481  // would be available there.
1482  if ( $db->getType() == 'oracle' ) {
1483  $db->query( 'BEGIN FILL_WIKI_INFO; END;', __METHOD__ );
1484  }
1485 
1486  Hooks::run( 'UnitTestsAfterDatabaseSetup', [ $db, $prefix ] );
1487  }
1488 
1495  protected static function setupExternalStoreTestDBs( $testPrefix = null ) {
1496  $connections = self::getExternalStoreDatabaseConnections();
1497  foreach ( $connections as $dbw ) {
1498  self::setupDatabaseWithTestPrefix( $dbw, $testPrefix );
1499  }
1500  }
1501 
1508  protected static function getExternalStoreDatabaseConnections() {
1509  global $wgDefaultExternalStore;
1510 
1512  $externalStoreDB = ExternalStore::getStoreObject( 'DB' );
1513  $defaultArray = (array)$wgDefaultExternalStore;
1514  $dbws = [];
1515  foreach ( $defaultArray as $url ) {
1516  if ( strpos( $url, 'DB://' ) === 0 ) {
1517  list( $proto, $cluster ) = explode( '://', $url, 2 );
1518  // Avoid getMaster() because setupDatabaseWithTestPrefix()
1519  // requires Database instead of plain DBConnRef/IDatabase
1520  $dbws[] = $externalStoreDB->getMaster( $cluster );
1521  }
1522  }
1523 
1524  return $dbws;
1525  }
1526 
1532  protected static function isUsingExternalStoreDB() {
1533  global $wgDefaultExternalStore;
1534  if ( !$wgDefaultExternalStore ) {
1535  return false;
1536  }
1537 
1538  $defaultArray = (array)$wgDefaultExternalStore;
1539  foreach ( $defaultArray as $url ) {
1540  if ( strpos( $url, 'DB://' ) === 0 ) {
1541  return true;
1542  }
1543  }
1544 
1545  return false;
1546  }
1547 
1555  if ( $db->tablePrefix() !== $this->dbPrefix() ) {
1556  throw new LogicException(
1557  'Trying to delete mock tables, but table prefix does not indicate a mock database.'
1558  );
1559  }
1560  }
1561 
1562  private static $schemaOverrideDefaults = [
1563  'scripts' => [],
1564  'create' => [],
1565  'drop' => [],
1566  'alter' => [],
1567  ];
1568 
1584  return [];
1585  }
1586 
1594  private function undoSchemaOverrides( IMaintainableDatabase $db, $oldOverrides ) {
1595  $this->ensureMockDatabaseConnection( $db );
1596 
1597  $oldOverrides = $oldOverrides + self::$schemaOverrideDefaults;
1598  $originalTables = $this->listOriginalTables( $db );
1599 
1600  // Drop tables that need to be restored or removed.
1601  $tablesToDrop = array_merge( $oldOverrides['create'], $oldOverrides['alter'] );
1602 
1603  // Restore tables that have been dropped or created or altered,
1604  // if they exist in the original schema.
1605  $tablesToRestore = array_merge( $tablesToDrop, $oldOverrides['drop'] );
1606  $tablesToRestore = array_intersect( $originalTables, $tablesToRestore );
1607 
1608  if ( $tablesToDrop ) {
1609  $this->dropMockTables( $db, $tablesToDrop );
1610  }
1611 
1612  if ( $tablesToRestore ) {
1613  $this->recloneMockTables( $db, $tablesToRestore );
1614  }
1615  }
1616 
1622  private function setUpSchema( IMaintainableDatabase $db ) {
1623  // Undo any active overrides.
1624  $oldOverrides = $db->_schemaOverrides ?? self::$schemaOverrideDefaults;
1625 
1626  if ( $oldOverrides['alter'] || $oldOverrides['create'] || $oldOverrides['drop'] ) {
1627  $this->undoSchemaOverrides( $db, $oldOverrides );
1628  }
1629 
1630  // Determine new overrides.
1631  $overrides = $this->getSchemaOverrides( $db ) + self::$schemaOverrideDefaults;
1632 
1633  $extraKeys = array_diff(
1634  array_keys( $overrides ),
1635  array_keys( self::$schemaOverrideDefaults )
1636  );
1637 
1638  if ( $extraKeys ) {
1639  throw new InvalidArgumentException(
1640  'Schema override contains extra keys: ' . var_export( $extraKeys, true )
1641  );
1642  }
1643 
1644  if ( !$overrides['scripts'] ) {
1645  // no scripts to run
1646  return;
1647  }
1648 
1649  if ( !$overrides['create'] && !$overrides['drop'] && !$overrides['alter'] ) {
1650  throw new InvalidArgumentException(
1651  'Schema override scripts given, but no tables are declared to be '
1652  . 'created, dropped or altered.'
1653  );
1654  }
1655 
1656  $this->ensureMockDatabaseConnection( $db );
1657 
1658  // Drop the tables that will be created by the schema scripts.
1659  $originalTables = $this->listOriginalTables( $db );
1660  $tablesToDrop = array_intersect( $originalTables, $overrides['create'] );
1661 
1662  if ( $tablesToDrop ) {
1663  $this->dropMockTables( $db, $tablesToDrop );
1664  }
1665 
1666  // Run schema override scripts.
1667  foreach ( $overrides['scripts'] as $script ) {
1668  $db->sourceFile(
1669  $script,
1670  null,
1671  null,
1672  __METHOD__,
1673  function ( $cmd ) {
1674  return $this->mungeSchemaUpdateQuery( $cmd );
1675  }
1676  );
1677  }
1678 
1679  $db->_schemaOverrides = $overrides;
1680  }
1681 
1682  private function mungeSchemaUpdateQuery( $cmd ) {
1683  return self::$useTemporaryTables
1684  ? preg_replace( '/\bCREATE\s+TABLE\b/i', 'CREATE TEMPORARY TABLE', $cmd )
1685  : $cmd;
1686  }
1687 
1695  $this->ensureMockDatabaseConnection( $db );
1696 
1697  foreach ( $tables as $tbl ) {
1698  $tbl = $db->tableName( $tbl );
1699  $db->query( "DROP TABLE IF EXISTS $tbl", __METHOD__ );
1700  }
1701  }
1702 
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 
1716  $unittestPrefixRegex = '/^' . preg_quote( $this->dbPrefix(), '/' ) . '/';
1717  $originalPrefixRegex = '/^' . preg_quote( $db->_originalTablePrefix, '/' ) . '/';
1718 
1719  $originalTables = array_filter(
1720  $originalTables,
1721  function ( $pt ) use ( $unittestPrefixRegex ) {
1722  return !preg_match( $unittestPrefixRegex, $pt );
1723  }
1724  );
1725 
1726  $originalTables = array_map(
1727  function ( $pt ) use ( $originalPrefixRegex ) {
1728  return preg_replace( $originalPrefixRegex, '', $pt );
1729  },
1730  $originalTables
1731  );
1732 
1733  return array_unique( $originalTables );
1734  }
1735 
1745  $this->ensureMockDatabaseConnection( $db );
1746 
1747  if ( !isset( $db->_originalTablePrefix ) ) {
1748  throw new LogicException( 'No original table prefix know, cannot restore tables!' );
1749  }
1750 
1751  $originalTables = $this->listOriginalTables( $db );
1752  $tables = array_intersect( $tables, $originalTables );
1753 
1754  $dbClone = new CloneDatabase( $db, $tables, $db->tablePrefix(), $db->_originalTablePrefix );
1755  $dbClone->useTemporaryTables( self::$useTemporaryTables );
1756 
1757  $dbClone->cloneTableStructure();
1758  }
1759 
1766  private function resetDB( $db, $tablesUsed ) {
1767  if ( $db ) {
1768  $userTables = [ 'user', 'user_groups', 'user_properties', 'actor' ];
1769  $pageTables = [
1770  'page', 'revision', 'ip_changes', 'revision_comment_temp', 'comment', 'archive',
1771  'revision_actor_temp', 'slots', 'content', 'content_models', 'slot_roles',
1772  ];
1773  $coreDBDataTables = array_merge( $userTables, $pageTables );
1774 
1775  // If any of the user or page tables were marked as used, we should clear all of them.
1776  if ( array_intersect( $tablesUsed, $userTables ) ) {
1777  $tablesUsed = array_unique( array_merge( $tablesUsed, $userTables ) );
1779  }
1780  if ( array_intersect( $tablesUsed, $pageTables ) ) {
1781  $tablesUsed = array_unique( array_merge( $tablesUsed, $pageTables ) );
1782  }
1783 
1784  // Postgres, Oracle, and MSSQL all use mwuser/pagecontent
1785  // instead of user/text. But Postgres does not remap the
1786  // table name in tableExists(), so we mark the real table
1787  // names as being used.
1788  if ( $db->getType() === 'postgres' ) {
1789  if ( in_array( 'user', $tablesUsed ) ) {
1790  $tablesUsed[] = 'mwuser';
1791  }
1792  if ( in_array( 'text', $tablesUsed ) ) {
1793  $tablesUsed[] = 'pagecontent';
1794  }
1795  }
1796 
1797  foreach ( $tablesUsed as $tbl ) {
1798  $this->truncateTable( $tbl, $db );
1799  }
1800 
1801  if ( array_intersect( $tablesUsed, $coreDBDataTables ) ) {
1802  // Reset services that may contain information relating to the truncated tables
1803  $this->overrideMwServices();
1804  // Re-add core DB data that was deleted
1805  $this->addCoreDBData();
1806  }
1807  }
1808  }
1809 
1818  protected function truncateTable( $tableName, IDatabase $db = null ) {
1819  if ( !$db ) {
1820  $db = $this->db;
1821  }
1822 
1823  if ( !$db->tableExists( $tableName ) ) {
1824  return;
1825  }
1826 
1827  $truncate = in_array( $db->getType(), [ 'oracle', 'mysql' ] );
1828 
1829  if ( $truncate ) {
1830  $db->query( 'TRUNCATE TABLE ' . $db->tableName( $tableName ), __METHOD__ );
1831  } else {
1832  $db->delete( $tableName, '*', __METHOD__ );
1833  }
1834 
1835  if ( $db instanceof DatabasePostgres || $db instanceof DatabaseSqlite ) {
1836  // Reset the table's sequence too.
1837  $db->resetSequenceForTable( $tableName, __METHOD__ );
1838  }
1839 
1840  // re-initialize site_stats table
1841  if ( $tableName === 'site_stats' ) {
1843  }
1844  }
1845 
1846  private static function unprefixTable( &$tableName, $ind, $prefix ) {
1847  $tableName = substr( $tableName, strlen( $prefix ) );
1848  }
1849 
1850  private static function isNotUnittest( $table ) {
1851  return strpos( $table, self::DB_PREFIX ) !== 0;
1852  }
1853 
1861  public static function listTables( IMaintainableDatabase $db ) {
1862  $prefix = $db->tablePrefix();
1863  $tables = $db->listTables( $prefix, __METHOD__ );
1864 
1865  if ( $db->getType() === 'mysql' ) {
1866  static $viewListCache = null;
1867  if ( $viewListCache === null ) {
1868  $viewListCache = $db->listViews( null, __METHOD__ );
1869  }
1870  // T45571: cannot clone VIEWs under MySQL
1871  $tables = array_diff( $tables, $viewListCache );
1872  }
1873  array_walk( $tables, [ __CLASS__, 'unprefixTable' ], $prefix );
1874 
1875  // Don't duplicate test tables from the previous fataled run
1876  $tables = array_filter( $tables, [ __CLASS__, 'isNotUnittest' ] );
1877 
1878  if ( $db->getType() == 'sqlite' ) {
1879  $tables = array_flip( $tables );
1880  // these are subtables of searchindex and don't need to be duped/dropped separately
1881  unset( $tables['searchindex_content'] );
1882  unset( $tables['searchindex_segdir'] );
1883  unset( $tables['searchindex_segments'] );
1884  $tables = array_flip( $tables );
1885  }
1886 
1887  return $tables;
1888  }
1889 
1898  public function copyTestData( IDatabase $source, IDatabase $target ) {
1899  if ( $this->db->getType() === 'sqlite' ) {
1900  // SQLite uses a non-temporary copy of the searchindex table for testing,
1901  // which gets deleted and re-created when setting up the secondary connection,
1902  // causing "Error 17" when trying to copy the data. See T191863#4130112.
1903  throw new RuntimeException(
1904  'Setting up a secondary database connection with test data is currently not'
1905  . 'with SQLite. You may want to use markTestSkippedIfDbType() to bypass this issue.'
1906  );
1907  }
1908 
1909  $tables = self::listOriginalTables( $source );
1910 
1911  foreach ( $tables as $table ) {
1912  $res = $source->select( $table, '*', [], __METHOD__ );
1913  $allRows = [];
1914 
1915  foreach ( $res as $row ) {
1916  $allRows[] = (array)$row;
1917  }
1918 
1919  $target->insert( $table, $allRows, __METHOD__, [ 'IGNORE' ] );
1920  }
1921  }
1922 
1927  protected function checkDbIsSupported() {
1928  if ( !in_array( $this->db->getType(), $this->supportedDBs ) ) {
1929  throw new MWException( $this->db->getType() . " is not currently supported for unit testing." );
1930  }
1931  }
1932 
1938  public function getCliArg( $offset ) {
1939  return $this->cliArgs[$offset] ?? null;
1940  }
1941 
1947  public function setCliArg( $offset, $value ) {
1948  $this->cliArgs[$offset] = $value;
1949  }
1950 
1958  public function hideDeprecated( $function ) {
1959  Wikimedia\suppressWarnings();
1960  wfDeprecated( $function );
1961  Wikimedia\restoreWarnings();
1962  }
1963 
1984  protected function assertSelect(
1985  $table, $fields, $condition, array $expectedRows, array $options = [], array $join_conds = []
1986  ) {
1987  if ( !$this->needsDB() ) {
1988  throw new MWException( 'When testing database state, the test cases\'s needDB()' .
1989  ' method should return true. Use @group Database or $this->tablesUsed.' );
1990  }
1991 
1992  $db = wfGetDB( DB_REPLICA );
1993 
1994  $res = $db->select(
1995  $table,
1996  $fields,
1997  $condition,
1998  wfGetCaller(),
1999  $options + [ 'ORDER BY' => $fields ],
2000  $join_conds
2001  );
2002  $this->assertNotEmpty( $res, "query failed: " . $db->lastError() );
2003 
2004  $i = 0;
2005 
2006  foreach ( $expectedRows as $expected ) {
2007  $r = $res->fetchRow();
2008  self::stripStringKeys( $r );
2009 
2010  $i += 1;
2011  $this->assertNotEmpty( $r, "row #$i missing" );
2012 
2013  $this->assertEquals( $expected, $r, "row #$i mismatches" );
2014  }
2015 
2016  $r = $res->fetchRow();
2017  self::stripStringKeys( $r );
2018 
2019  $this->assertFalse( $r, "found extra row (after #$i)" );
2020  }
2021 
2033  protected function arrayWrap( array $elements ) {
2034  return array_map(
2035  function ( $element ) {
2036  return [ $element ];
2037  },
2038  $elements
2039  );
2040  }
2041 
2054  protected function assertArrayEquals( array $expected, array $actual,
2055  $ordered = false, $named = false
2056  ) {
2057  if ( !$ordered ) {
2058  $this->objectAssociativeSort( $expected );
2059  $this->objectAssociativeSort( $actual );
2060  }
2061 
2062  if ( !$named ) {
2063  $expected = array_values( $expected );
2064  $actual = array_values( $actual );
2065  }
2066 
2067  call_user_func_array(
2068  [ $this, 'assertEquals' ],
2069  array_merge( [ $expected, $actual ], array_slice( func_get_args(), 4 ) )
2070  );
2071  }
2072 
2085  protected function assertHTMLEquals( $expected, $actual, $msg = '' ) {
2086  $expected = str_replace( '>', ">\n", $expected );
2087  $actual = str_replace( '>', ">\n", $actual );
2088 
2089  $this->assertEquals( $expected, $actual, $msg );
2090  }
2091 
2099  protected function objectAssociativeSort( array &$array ) {
2100  uasort(
2101  $array,
2102  function ( $a, $b ) {
2103  return serialize( $a ) <=> serialize( $b );
2104  }
2105  );
2106  }
2107 
2117  protected static function stripStringKeys( &$r ) {
2118  if ( !is_array( $r ) ) {
2119  return;
2120  }
2121 
2122  foreach ( $r as $k => $v ) {
2123  if ( is_string( $k ) ) {
2124  unset( $r[$k] );
2125  }
2126  }
2127  }
2128 
2142  protected function assertTypeOrValue( $type, $actual, $value = false, $message = '' ) {
2143  if ( $actual === $value ) {
2144  $this->assertTrue( true, $message );
2145  } else {
2146  $this->assertType( $type, $actual, $message );
2147  }
2148  }
2149 
2161  protected function assertType( $type, $actual, $message = '' ) {
2162  if ( class_exists( $type ) || interface_exists( $type ) ) {
2163  $this->assertInstanceOf( $type, $actual, $message );
2164  } else {
2165  $this->assertInternalType( $type, $actual, $message );
2166  }
2167  }
2168 
2178  protected function isWikitextNS( $ns ) {
2180 
2181  if ( isset( $wgNamespaceContentModels[$ns] ) ) {
2182  return $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT;
2183  }
2184 
2185  return true;
2186  }
2187 
2195  protected function getDefaultWikitextNS() {
2197 
2198  static $wikitextNS = null; // this is not going to change
2199  if ( $wikitextNS !== null ) {
2200  return $wikitextNS;
2201  }
2202 
2203  // quickly short out on most common case:
2204  if ( !isset( $wgNamespaceContentModels[NS_MAIN] ) ) {
2205  return NS_MAIN;
2206  }
2207 
2208  // NOTE: prefer content namespaces
2209  $namespaces = array_unique( array_merge(
2211  [ NS_MAIN, NS_HELP, NS_PROJECT ], // prefer these
2213  ) );
2214 
2215  $namespaces = array_diff( $namespaces, [
2216  NS_FILE, NS_CATEGORY, NS_MEDIAWIKI, NS_USER // don't mess with magic namespaces
2217  ] );
2218 
2219  $talk = array_filter( $namespaces, function ( $ns ) {
2220  return MWNamespace::isTalk( $ns );
2221  } );
2222 
2223  // prefer non-talk pages
2224  $namespaces = array_diff( $namespaces, $talk );
2225  $namespaces = array_merge( $namespaces, $talk );
2226 
2227  // check default content model of each namespace
2228  foreach ( $namespaces as $ns ) {
2229  if ( !isset( $wgNamespaceContentModels[$ns] ) ||
2230  $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT
2231  ) {
2232  $wikitextNS = $ns;
2233 
2234  return $wikitextNS;
2235  }
2236  }
2237 
2238  // give up
2239  // @todo Inside a test, we could skip the test as incomplete.
2240  // But frequently, this is used in fixture setup.
2241  throw new MWException( "No namespace defaults to wikitext!" );
2242  }
2243 
2250  protected function markTestSkippedIfNoDiff3() {
2251  global $wgDiff3;
2252 
2253  # This check may also protect against code injection in
2254  # case of broken installations.
2255  Wikimedia\suppressWarnings();
2256  $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 );
2257  Wikimedia\restoreWarnings();
2258 
2259  if ( !$haveDiff3 ) {
2260  $this->markTestSkipped( "Skip test, since diff3 is not configured" );
2261  }
2262  }
2263 
2272  protected function checkPHPExtension( $extName ) {
2273  $loaded = extension_loaded( $extName );
2274  if ( !$loaded ) {
2275  $this->markTestSkipped( "PHP extension '$extName' is not loaded, skipping." );
2276  }
2277 
2278  return $loaded;
2279  }
2280 
2287  protected function markTestSkippedIfDbType( $type ) {
2288  if ( $this->db->getType() === $type ) {
2289  $this->markTestSkipped( "The $type database type isn't supported for this test" );
2290  }
2291  }
2292 
2298  public static function wfResetOutputBuffersBarrier( $buffer ) {
2299  return $buffer;
2300  }
2301 
2309  protected function setTemporaryHook( $hookName, $handler ) {
2310  $this->mergeMwGlobalArrayValue( 'wgHooks', [ $hookName => [ $handler ] ] );
2311  }
2312 
2322  protected function assertFileContains(
2323  $fileName,
2324  $actualData,
2325  $createIfMissing = false,
2326  $msg = ''
2327  ) {
2328  if ( $createIfMissing ) {
2329  if ( !file_exists( $fileName ) ) {
2330  file_put_contents( $fileName, $actualData );
2331  $this->markTestSkipped( 'Data file $fileName does not exist' );
2332  }
2333  } else {
2334  self::assertFileExists( $fileName );
2335  }
2336  self::assertEquals( file_get_contents( $fileName ), $actualData, $msg );
2337  }
2338 
2351  protected function editPage( $pageName, $text, $summary = '', $defaultNs = NS_MAIN ) {
2352  if ( !$this->needsDB() ) {
2353  throw new MWException( 'When testing which pages, the test cases\'s needsDB()' .
2354  ' method should return true. Use @group Database or $this->tablesUsed.' );
2355  }
2356 
2357  $title = Title::newFromText( $pageName, $defaultNs );
2358  $page = WikiPage::factory( $title );
2359 
2360  return $page->doEditContent( ContentHandler::makeContent( $text, $title ), $summary );
2361  }
2362 
2371  protected function revisionDelete(
2372  $rev, array $value = [ Revision::DELETED_TEXT => 1 ], $comment = ''
2373  ) {
2374  if ( is_int( $rev ) ) {
2376  }
2378  'revision', RequestContext::getMain(), $rev->getTitle(), [ $rev->getId() ]
2379  )->setVisibility( [
2380  'value' => $value,
2381  'comment' => $comment,
2382  ] );
2383  }
2384 }
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:138
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:1175
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:2162
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:1266
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:979
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:1983
$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
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 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
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.
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
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:2210
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:2384
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:1985
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:925
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:1769
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:925
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:596
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:46
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:1753
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:2989
static unprefixTable(&$tableName, $ind, $prefix)
static clearCaches()
Intended for tests that may change configuration in a way that invalidates caches.
Definition: Language.php:284
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:728
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.
Wraps another spi to capture all logs generated.
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:118
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:1476
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.
listOriginalTables(IMaintainableDatabase $db)
Lists all tables in the live database schema, without a prefix.
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