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 
365  foreach ( $wgJobClasses as $type => $class ) {
366  JobQueueGroup::singleton()->get( $type )->delete();
367  }
369 
373 
374  // TODO: move global state into MediaWikiServices
376  if ( session_id() !== '' ) {
377  session_write_close();
378  session_id( '' );
379  }
380 
381  $wgRequest = new FauxRequest();
383  }
384 
385  public function run( PHPUnit_Framework_TestResult $result = null ) {
386  if ( $result instanceof MediaWikiTestResult ) {
387  $this->cliArgs = $result->getMediaWikiCliArgs();
388  }
389  $this->overrideMwServices();
390 
391  if ( $this->needsDB() && !$this->isTestInDatabaseGroup() ) {
392  throw new Exception(
393  get_class( $this ) . ' apparently needsDB but is not in the Database group'
394  );
395  }
396 
397  $needsResetDB = false;
398  if ( !self::$dbSetup || $this->needsDB() ) {
399  // set up a DB connection for this test to use
400 
401  self::$useTemporaryTables = !$this->getCliArg( 'use-normal-tables' );
402  self::$reuseDB = $this->getCliArg( 'reuse-db' );
403 
404  $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
405  $this->db = $lb->getConnection( DB_MASTER );
406 
407  $this->checkDbIsSupported();
408 
409  if ( !self::$dbSetup ) {
410  $this->setupAllTestDBs();
411  $this->addCoreDBData();
412  }
413 
414  // TODO: the DB setup should be done in setUpBeforeClass(), so the test DB
415  // is available in subclass's setUpBeforeClass() and setUp() methods.
416  // This would also remove the need for the HACK that is oncePerClass().
417  if ( $this->oncePerClass() ) {
418  $this->setUpSchema( $this->db );
419  $this->resetDB( $this->db, $this->tablesUsed );
420  $this->addDBDataOnce();
421  }
422 
423  $this->addDBData();
424  $needsResetDB = true;
425  }
426 
427  parent::run( $result );
428 
429  // We don't mind if we override already-overridden services during cleanup
430  $this->overriddenServices = [];
431 
432  if ( $needsResetDB ) {
433  $this->resetDB( $this->db, $this->tablesUsed );
434  }
435 
436  self::restoreMwServices();
437  $this->localServices = null;
438  }
439 
443  private function oncePerClass() {
444  // Remember current test class in the database connection,
445  // so we know when we need to run addData.
446 
447  $class = static::class;
448 
449  $first = !isset( $this->db->_hasDataForTestClass )
450  || $this->db->_hasDataForTestClass !== $class;
451 
452  $this->db->_hasDataForTestClass = $class;
453  return $first;
454  }
455 
461  public function usesTemporaryTables() {
462  return self::$useTemporaryTables;
463  }
464 
474  protected function getNewTempFile() {
475  $fileName = tempnam(
476  wfTempDir(),
477  // Avoid backslashes here as they result in inconsistent results
478  // between Windows and other OS, as well as between functions
479  // that try to normalise these in one or both directions.
480  // For example, tempnam rejects directory separators in the prefix which
481  // means it rejects any namespaced class on Windows.
482  // And then there is, wfMkdirParents which normalises paths always
483  // whereas most other PHP and MW functions do not.
484  'MW_PHPUnit_' . strtr( static::class, [ '\\' => '_' ] ) . '_'
485  );
486  $this->tmpFiles[] = $fileName;
487 
488  return $fileName;
489  }
490 
501  protected function getNewTempDirectory() {
502  // Starting of with a temporary *file*.
503  $fileName = $this->getNewTempFile();
504 
505  // Converting the temporary file to a *directory*.
506  // The following is not atomic, but at least we now have a single place,
507  // where temporary directory creation is bundled and can be improved.
508  unlink( $fileName );
509  // If this fails for some reason, PHP will warn and fail the test.
510  mkdir( $fileName, 0777, /* recursive = */ true );
511 
512  return $fileName;
513  }
514 
515  protected function setUp() {
516  parent::setUp();
517  $this->called['setUp'] = true;
518 
519  $this->phpErrorLevel = intval( ini_get( 'error_reporting' ) );
520 
521  $this->overriddenServices = [];
522 
523  // Cleaning up temporary files
524  foreach ( $this->tmpFiles as $fileName ) {
525  if ( is_file( $fileName ) || ( is_link( $fileName ) ) ) {
526  unlink( $fileName );
527  } elseif ( is_dir( $fileName ) ) {
528  wfRecursiveRemoveDir( $fileName );
529  }
530  }
531 
532  if ( $this->needsDB() && $this->db ) {
533  // Clean up open transactions
534  while ( $this->db->trxLevel() > 0 ) {
535  $this->db->rollback( __METHOD__, 'flush' );
536  }
537  // Check for unsafe queries
538  if ( $this->db->getType() === 'mysql' ) {
539  $this->db->query( "SET sql_mode = 'STRICT_ALL_TABLES'", __METHOD__ );
540  }
541  }
542 
543  // Reset all caches between tests.
544  self::resetNonServiceCaches();
545 
546  // XXX: reset maintenance triggers
547  // Hook into period lag checks which often happen in long-running scripts
548  $lbFactory = $this->localServices->getDBLoadBalancerFactory();
549  Maintenance::setLBFactoryTriggers( $lbFactory, $this->localServices->getMainConfig() );
550 
551  ob_start( 'MediaWikiTestCase::wfResetOutputBuffersBarrier' );
552  }
553 
554  protected function addTmpFiles( $files ) {
555  $this->tmpFiles = array_merge( $this->tmpFiles, (array)$files );
556  }
557 
558  // @todo Make const when we no longer support HHVM (T192166)
559  private static $namespaceAffectingSettings = [
560  'wgAllowImageMoving',
561  'wgCanonicalNamespaceNames',
562  'wgCapitalLinkOverrides',
563  'wgCapitalLinks',
564  'wgContentNamespaces',
565  'wgExtensionMessagesFiles',
566  'wgExtensionNamespaces',
567  'wgExtraNamespaces',
568  'wgExtraSignatureNamespaces',
569  'wgNamespaceContentModels',
570  'wgNamespaceProtection',
571  'wgNamespacesWithSubpages',
572  'wgNonincludableNamespaces',
573  'wgRestrictionLevels',
574  ];
575 
576  protected function tearDown() {
577  global $wgRequest, $wgSQLMode;
578 
579  $status = ob_get_status();
580  if ( isset( $status['name'] ) &&
581  $status['name'] === 'MediaWikiTestCase::wfResetOutputBuffersBarrier'
582  ) {
583  ob_end_flush();
584  }
585 
586  $this->called['tearDown'] = true;
587  // Cleaning up temporary files
588  foreach ( $this->tmpFiles as $fileName ) {
589  if ( is_file( $fileName ) || ( is_link( $fileName ) ) ) {
590  unlink( $fileName );
591  } elseif ( is_dir( $fileName ) ) {
592  wfRecursiveRemoveDir( $fileName );
593  }
594  }
595 
596  if ( $this->needsDB() && $this->db ) {
597  // Clean up open transactions
598  while ( $this->db->trxLevel() > 0 ) {
599  $this->db->rollback( __METHOD__, 'flush' );
600  }
601  if ( $this->db->getType() === 'mysql' ) {
602  $this->db->query( "SET sql_mode = " . $this->db->addQuotes( $wgSQLMode ),
603  __METHOD__ );
604  }
605  }
606 
607  // Re-enable any disabled deprecation warnings
609  // Restore mw globals
610  foreach ( $this->mwGlobals as $key => $value ) {
611  $GLOBALS[$key] = $value;
612  }
613  foreach ( $this->mwGlobalsToUnset as $value ) {
614  unset( $GLOBALS[$value] );
615  }
616  foreach ( $this->iniSettings as $name => $value ) {
617  ini_set( $name, $value );
618  }
619  if (
620  array_intersect( self::$namespaceAffectingSettings, array_keys( $this->mwGlobals ) ) ||
621  array_intersect( self::$namespaceAffectingSettings, $this->mwGlobalsToUnset )
622  ) {
623  $this->resetNamespaces();
624  }
625  $this->mwGlobals = [];
626  $this->mwGlobalsToUnset = [];
627  $this->restoreLoggers();
628 
629  // TODO: move global state into MediaWikiServices
631  if ( session_id() !== '' ) {
632  session_write_close();
633  session_id( '' );
634  }
635  $wgRequest = new FauxRequest();
638 
639  $phpErrorLevel = intval( ini_get( 'error_reporting' ) );
640 
641  if ( $phpErrorLevel !== $this->phpErrorLevel ) {
642  ini_set( 'error_reporting', $this->phpErrorLevel );
643 
644  $oldHex = strtoupper( dechex( $this->phpErrorLevel ) );
645  $newHex = strtoupper( dechex( $phpErrorLevel ) );
646  $message = "PHP error_reporting setting was left dirty: "
647  . "was 0x$oldHex before test, 0x$newHex after test!";
648 
649  $this->fail( $message );
650  }
651 
652  parent::tearDown();
653  }
654 
663  final public function testMediaWikiTestCaseParentSetupCalled() {
664  $this->assertArrayHasKey( 'setUp', $this->called,
665  static::class . '::setUp() must call parent::setUp()'
666  );
667  }
668 
678  protected function setService( $name, $object ) {
679  if ( !$this->localServices ) {
680  throw new Exception( __METHOD__ . ' must be called after MediaWikiTestCase::run()' );
681  }
682 
683  if ( $this->localServices !== MediaWikiServices::getInstance() ) {
684  throw new Exception( __METHOD__ . ' will not work because the global MediaWikiServices '
685  . 'instance has been replaced by test code.' );
686  }
687 
688  $this->overriddenServices[] = $name;
689 
690  $this->localServices->disableService( $name );
691  $this->localServices->redefineService(
692  $name,
693  function () use ( $object ) {
694  return $object;
695  }
696  );
697 
698  if ( $name === 'ContentLanguage' ) {
699  $this->doSetMwGlobals( [ 'wgContLang' => $object ] );
700  }
701  }
702 
738  protected function setMwGlobals( $pairs, $value = null ) {
739  if ( is_string( $pairs ) ) {
740  $pairs = [ $pairs => $value ];
741  }
742 
743  if ( isset( $pairs['wgContLang'] ) ) {
744  throw new MWException(
745  'No setting $wgContLang, use setContentLang() or setService( \'ContentLanguage\' )'
746  );
747  }
748 
749  $this->doSetMwGlobals( $pairs, $value );
750  }
751 
756  private function doSetMwGlobals( $pairs, $value = null ) {
757  $this->doStashMwGlobals( array_keys( $pairs ) );
758 
759  foreach ( $pairs as $key => $value ) {
760  $GLOBALS[$key] = $value;
761  }
762 
763  if ( array_intersect( self::$namespaceAffectingSettings, array_keys( $pairs ) ) ) {
764  $this->resetNamespaces();
765  }
766  }
767 
774  protected function setIniSetting( $name, $value ) {
775  $original = ini_get( $name );
776  $this->iniSettings[$name] = $original;
777  ini_set( $name, $value );
778  }
779 
784  private function resetNamespaces() {
785  if ( !$this->localServices ) {
786  throw new Exception( __METHOD__ . ' must be called after MediaWikiTestCase::run()' );
787  }
788 
789  if ( $this->localServices !== MediaWikiServices::getInstance() ) {
790  throw new Exception( __METHOD__ . ' will not work because the global MediaWikiServices '
791  . 'instance has been replaced by test code.' );
792  }
793 
795  }
796 
805  private static function canShallowCopy( $value ) {
806  if ( is_scalar( $value ) || $value === null ) {
807  return true;
808  }
809  if ( is_array( $value ) ) {
810  foreach ( $value as $subValue ) {
811  if ( !is_scalar( $subValue ) && $subValue !== null ) {
812  return false;
813  }
814  }
815  return true;
816  }
817  return false;
818  }
819 
820  private function doStashMwGlobals( $globalKeys ) {
821  if ( is_string( $globalKeys ) ) {
822  $globalKeys = [ $globalKeys ];
823  }
824 
825  foreach ( $globalKeys as $globalKey ) {
826  // NOTE: make sure we only save the global once or a second call to
827  // setMwGlobals() on the same global would override the original
828  // value.
829  if (
830  !array_key_exists( $globalKey, $this->mwGlobals ) &&
831  !array_key_exists( $globalKey, $this->mwGlobalsToUnset )
832  ) {
833  if ( !array_key_exists( $globalKey, $GLOBALS ) ) {
834  $this->mwGlobalsToUnset[$globalKey] = $globalKey;
835  continue;
836  }
837  // NOTE: we serialize then unserialize the value in case it is an object
838  // this stops any objects being passed by reference. We could use clone
839  // and if is_object but this does account for objects within objects!
840  if ( self::canShallowCopy( $GLOBALS[$globalKey] ) ) {
841  $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
842  } elseif (
843  // Many MediaWiki types are safe to clone. These are the
844  // ones that are most commonly stashed.
845  $GLOBALS[$globalKey] instanceof Language ||
846  $GLOBALS[$globalKey] instanceof User ||
847  $GLOBALS[$globalKey] instanceof FauxRequest
848  ) {
849  $this->mwGlobals[$globalKey] = clone $GLOBALS[$globalKey];
850  } elseif ( $this->containsClosure( $GLOBALS[$globalKey] ) ) {
851  // Serializing Closure only gives a warning on HHVM while
852  // it throws an Exception on Zend.
853  // Workaround for https://github.com/facebook/hhvm/issues/6206
854  $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
855  } else {
856  try {
857  $this->mwGlobals[$globalKey] = unserialize( serialize( $GLOBALS[$globalKey] ) );
858  } catch ( Exception $e ) {
859  $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
860  }
861  }
862  }
863  }
864  }
865 
872  private function containsClosure( $var, $maxDepth = 15 ) {
873  if ( $var instanceof Closure ) {
874  return true;
875  }
876  if ( !is_array( $var ) || $maxDepth === 0 ) {
877  return false;
878  }
879 
880  foreach ( $var as $value ) {
881  if ( $this->containsClosure( $value, $maxDepth - 1 ) ) {
882  return true;
883  }
884  }
885  return false;
886  }
887 
903  protected function mergeMwGlobalArrayValue( $name, $values ) {
904  if ( !isset( $GLOBALS[$name] ) ) {
905  $merged = $values;
906  } else {
907  if ( !is_array( $GLOBALS[$name] ) ) {
908  throw new MWException( "MW global $name is not an array." );
909  }
910 
911  // NOTE: do not use array_merge, it screws up for numeric keys.
912  $merged = $GLOBALS[$name];
913  foreach ( $values as $k => $v ) {
914  $merged[$k] = $v;
915  }
916  }
917 
918  $this->setMwGlobals( $name, $merged );
919  }
920 
936  protected function overrideMwServices(
937  Config $configOverrides = null, array $services = []
938  ) {
939  if ( $this->overriddenServices ) {
940  throw new MWException(
941  'The following services were set and are now being unset by overrideMwServices: ' .
942  implode( ', ', $this->overriddenServices )
943  );
944  }
945  $newInstance = self::installMockMwServices( $configOverrides );
946 
947  if ( $this->localServices ) {
948  $this->localServices->destroy();
949  }
950 
951  $this->localServices = $newInstance;
952 
953  foreach ( $services as $name => $callback ) {
954  $newInstance->redefineService( $name, $callback );
955  }
956 
957  self::resetGlobalParser();
958 
959  return $newInstance;
960  }
961 
979  public static function installMockMwServices( Config $configOverrides = null ) {
980  // Make sure we have the original service locator
981  if ( !self::$originalServices ) {
982  self::$originalServices = MediaWikiServices::getInstance();
983  }
984 
985  if ( !$configOverrides ) {
986  $configOverrides = new HashConfig();
987  }
988 
989  $oldConfigFactory = self::$originalServices->getConfigFactory();
990  $oldLoadBalancerFactory = self::$originalServices->getDBLoadBalancerFactory();
991 
992  $testConfig = self::makeTestConfig( null, $configOverrides );
993  $newServices = new MediaWikiServices( $testConfig );
994 
995  // Load the default wiring from the specified files.
996  // NOTE: this logic mirrors the logic in MediaWikiServices::newInstance.
997  $wiringFiles = $testConfig->get( 'ServiceWiringFiles' );
998  $newServices->loadWiringFiles( $wiringFiles );
999 
1000  // Provide a traditional hook point to allow extensions to configure services.
1001  Hooks::run( 'MediaWikiServices', [ $newServices ] );
1002 
1003  // Use bootstrap config for all configuration.
1004  // This allows config overrides via global variables to take effect.
1005  $bootstrapConfig = $newServices->getBootstrapConfig();
1006  $newServices->resetServiceForTesting( 'ConfigFactory' );
1007  $newServices->redefineService(
1008  'ConfigFactory',
1009  self::makeTestConfigFactoryInstantiator(
1010  $oldConfigFactory,
1011  [ 'main' => $bootstrapConfig ]
1012  )
1013  );
1014  $newServices->resetServiceForTesting( 'DBLoadBalancerFactory' );
1015  $newServices->redefineService(
1016  'DBLoadBalancerFactory',
1017  function ( MediaWikiServices $services ) use ( $oldLoadBalancerFactory ) {
1018  return $oldLoadBalancerFactory;
1019  }
1020  );
1021 
1022  MediaWikiServices::forceGlobalInstance( $newServices );
1023 
1024  self::resetGlobalParser();
1025 
1026  return $newServices;
1027  }
1028 
1040  public static function restoreMwServices() {
1041  if ( !self::$originalServices ) {
1042  return false;
1043  }
1044 
1045  $currentServices = MediaWikiServices::getInstance();
1046 
1047  if ( self::$originalServices === $currentServices ) {
1048  return false;
1049  }
1050 
1051  MediaWikiServices::forceGlobalInstance( self::$originalServices );
1052  $currentServices->destroy();
1053 
1054  self::resetGlobalParser();
1055 
1056  return true;
1057  }
1058 
1063  private static function resetGlobalParser() {
1064  // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgParser
1065  global $wgParser;
1066  if ( $wgParser instanceof StubObject ) {
1067  return;
1068  }
1069  $wgParser = new StubObject( 'wgParser', function () {
1070  return MediaWikiServices::getInstance()->getParser();
1071  } );
1072  }
1073 
1078  public function setUserLang( $lang ) {
1079  RequestContext::getMain()->setLanguage( $lang );
1080  $this->setMwGlobals( 'wgLang', RequestContext::getMain()->getLanguage() );
1081  }
1082 
1087  public function setContentLang( $lang ) {
1088  if ( $lang instanceof Language ) {
1089  $this->setMwGlobals( 'wgLanguageCode', $lang->getCode() );
1090  // Set to the exact object requested
1091  $this->setService( 'ContentLanguage', $lang );
1092  } else {
1093  $this->setMwGlobals( 'wgLanguageCode', $lang );
1094  // Let the service handler make up the object. Avoid calling setService(), because if
1095  // we do, overrideMwServices() will complain if it's called later on.
1096  $services = MediaWikiServices::getInstance();
1097  $services->resetServiceForTesting( 'ContentLanguage' );
1098  $this->doSetMwGlobals( [ 'wgContLang' => $services->getContentLanguage() ] );
1099  }
1100  }
1101 
1116  public function setGroupPermissions( $newPerms, $newKey = null, $newValue = null ) {
1117  global $wgGroupPermissions;
1118 
1119  if ( is_string( $newPerms ) ) {
1120  $newPerms = [ $newPerms => [ $newKey => $newValue ] ];
1121  }
1122 
1123  $newPermissions = $wgGroupPermissions;
1124  foreach ( $newPerms as $group => $permissions ) {
1125  foreach ( $permissions as $key => $value ) {
1126  $newPermissions[$group][$key] = $value;
1127  }
1128  }
1129 
1130  $this->setMwGlobals( 'wgGroupPermissions', $newPermissions );
1131  }
1132 
1139  protected function setLogger( $channel, LoggerInterface $logger ) {
1140  // TODO: Once loggers are managed by MediaWikiServices, use
1141  // overrideMwServices() to set loggers.
1142 
1143  $provider = LoggerFactory::getProvider();
1144  $wrappedProvider = TestingAccessWrapper::newFromObject( $provider );
1145  $singletons = $wrappedProvider->singletons;
1146  if ( $provider instanceof MonologSpi ) {
1147  if ( !isset( $this->loggers[$channel] ) ) {
1148  $this->loggers[$channel] = $singletons['loggers'][$channel] ?? null;
1149  }
1150  $singletons['loggers'][$channel] = $logger;
1151  } elseif ( $provider instanceof LegacySpi || $provider instanceof LogCapturingSpi ) {
1152  if ( !isset( $this->loggers[$channel] ) ) {
1153  $this->loggers[$channel] = $singletons[$channel] ?? null;
1154  }
1155  $singletons[$channel] = $logger;
1156  } else {
1157  throw new LogicException( __METHOD__ . ': setting a logger for ' . get_class( $provider )
1158  . ' is not implemented' );
1159  }
1160  $wrappedProvider->singletons = $singletons;
1161  }
1162 
1167  private function restoreLoggers() {
1168  $provider = LoggerFactory::getProvider();
1169  $wrappedProvider = TestingAccessWrapper::newFromObject( $provider );
1170  $singletons = $wrappedProvider->singletons;
1171  foreach ( $this->loggers as $channel => $logger ) {
1172  if ( $provider instanceof MonologSpi ) {
1173  if ( $logger === null ) {
1174  unset( $singletons['loggers'][$channel] );
1175  } else {
1176  $singletons['loggers'][$channel] = $logger;
1177  }
1178  } elseif ( $provider instanceof LegacySpi || $provider instanceof LogCapturingSpi ) {
1179  if ( $logger === null ) {
1180  unset( $singletons[$channel] );
1181  } else {
1182  $singletons[$channel] = $logger;
1183  }
1184  }
1185  }
1186  $wrappedProvider->singletons = $singletons;
1187  $this->loggers = [];
1188  }
1189 
1194  public function dbPrefix() {
1195  return self::getTestPrefixFor( $this->db );
1196  }
1197 
1203  public static function getTestPrefixFor( IDatabase $db ) {
1204  return $db->getType() == 'oracle' ? self::ORA_DB_PREFIX : self::DB_PREFIX;
1205  }
1206 
1211  public function needsDB() {
1212  // If the test says it uses database tables, it needs the database
1213  return $this->tablesUsed || $this->isTestInDatabaseGroup();
1214  }
1215 
1220  protected function isTestInDatabaseGroup() {
1221  // If the test class says it belongs to the Database group, it needs the database.
1222  // NOTE: This ONLY checks for the group in the class level doc comment.
1223  $rc = new ReflectionClass( $this );
1224  return (bool)preg_match( '/@group +Database/im', $rc->getDocComment() );
1225  }
1226 
1243  protected function insertPage(
1244  $pageName,
1245  $text = 'Sample page for unit test.',
1246  $namespace = null,
1247  User $user = null
1248  ) {
1249  if ( !$this->needsDB() ) {
1250  throw new MWException( 'When testing which pages, the test cases\'s needsDB()' .
1251  ' method should return true. Use @group Database or $this->tablesUsed.' );
1252  }
1253 
1254  if ( is_string( $pageName ) ) {
1255  $title = Title::newFromText( $pageName, $namespace );
1256  } else {
1257  $title = $pageName;
1258  }
1259 
1260  if ( !$user ) {
1261  $user = static::getTestSysop()->getUser();
1262  }
1263  $comment = __METHOD__ . ': Sample page for unit test.';
1264 
1265  $page = WikiPage::factory( $title );
1266  $page->doEditContent( ContentHandler::makeContent( $text, $title ), $comment, 0, false, $user );
1267 
1268  return [
1269  'title' => $title,
1270  'id' => $page->getId(),
1271  ];
1272  }
1273 
1289  public function addDBDataOnce() {
1290  }
1291 
1301  public function addDBData() {
1302  }
1303 
1307  protected function addCoreDBData() {
1308  if ( $this->db->getType() == 'oracle' ) {
1309  # Insert 0 user to prevent FK violations
1310  # Anonymous user
1311  if ( !$this->db->selectField( 'user', '1', [ 'user_id' => 0 ] ) ) {
1312  $this->db->insert( 'user', [
1313  'user_id' => 0,
1314  'user_name' => 'Anonymous' ], __METHOD__, [ 'IGNORE' ] );
1315  }
1316 
1317  # Insert 0 page to prevent FK violations
1318  # Blank page
1319  if ( !$this->db->selectField( 'page', '1', [ 'page_id' => 0 ] ) ) {
1320  $this->db->insert( 'page', [
1321  'page_id' => 0,
1322  'page_namespace' => 0,
1323  'page_title' => ' ',
1324  'page_restrictions' => null,
1325  'page_is_redirect' => 0,
1326  'page_is_new' => 0,
1327  'page_random' => 0,
1328  'page_touched' => $this->db->timestamp(),
1329  'page_latest' => 0,
1330  'page_len' => 0 ], __METHOD__, [ 'IGNORE' ] );
1331  }
1332  }
1333 
1335 
1337 
1338  // Make sysop user
1339  $user = static::getTestSysop()->getUser();
1340 
1341  // Make 1 page with 1 revision
1342  $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
1343  if ( $page->getId() == 0 ) {
1344  $page->doEditContent(
1345  new WikitextContent( 'UTContent' ),
1346  'UTPageSummary',
1348  false,
1349  $user
1350  );
1351  // an edit always attempt to purge backlink links such as history
1352  // pages. That is unnecessary.
1353  JobQueueGroup::singleton()->get( 'htmlCacheUpdate' )->delete();
1354  // WikiPages::doEditUpdates randomly adds RC purges
1355  JobQueueGroup::singleton()->get( 'recentChangesUpdate' )->delete();
1356 
1357  // doEditContent() probably started the session via
1358  // User::loadFromSession(). Close it now.
1359  if ( session_id() !== '' ) {
1360  session_write_close();
1361  session_id( '' );
1362  }
1363  }
1364  }
1365 
1375  public static function teardownTestDB() {
1376  global $wgJobClasses;
1377 
1378  if ( !self::$dbSetup ) {
1379  return;
1380  }
1381 
1382  Hooks::run( 'UnitTestsBeforeDatabaseTeardown' );
1383 
1384  foreach ( $wgJobClasses as $type => $class ) {
1385  // Delete any jobs under the clone DB (or old prefix in other stores)
1386  JobQueueGroup::singleton()->get( $type )->delete();
1387  }
1388 
1389  // T219673: close any connections from code that failed to call reuseConnection()
1390  // or is still holding onto a DBConnRef instance (e.g. in a singleton).
1391  MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->closeAll();
1392  CloneDatabase::changePrefix( self::$oldTablePrefix );
1393 
1394  self::$oldTablePrefix = false;
1395  self::$dbSetup = false;
1396  }
1397 
1409  protected static function setupDatabaseWithTestPrefix(
1411  $prefix = null
1412  ) {
1413  if ( $prefix === null ) {
1414  $prefix = self::getTestPrefixFor( $db );
1415  }
1416 
1417  if ( ( $db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
1418  $db->tablePrefix( $prefix );
1419  return false;
1420  }
1421 
1422  if ( !isset( $db->_originalTablePrefix ) ) {
1423  $oldPrefix = $db->tablePrefix();
1424 
1425  if ( $oldPrefix === $prefix ) {
1426  // table already has the correct prefix, but presumably no cloned tables
1427  $oldPrefix = self::$oldTablePrefix;
1428  }
1429 
1430  $db->tablePrefix( $oldPrefix );
1431  $tablesCloned = self::listTables( $db );
1432  $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix, $oldPrefix );
1433  $dbClone->useTemporaryTables( self::$useTemporaryTables );
1434 
1435  $dbClone->cloneTableStructure();
1436 
1437  $db->tablePrefix( $prefix );
1438  $db->_originalTablePrefix = $oldPrefix;
1439  }
1440 
1441  return true;
1442  }
1443 
1447  public function setupAllTestDBs() {
1448  global $wgDBprefix;
1449 
1450  self::$oldTablePrefix = $wgDBprefix;
1451 
1452  $testPrefix = $this->dbPrefix();
1453 
1454  // switch to a temporary clone of the database
1455  self::setupTestDB( $this->db, $testPrefix );
1456 
1457  if ( self::isUsingExternalStoreDB() ) {
1458  self::setupExternalStoreTestDBs( $testPrefix );
1459  }
1460 
1461  // NOTE: Change the prefix in the LBFactory and $wgDBprefix, to prevent
1462  // *any* database connections to operate on live data.
1463  CloneDatabase::changePrefix( $testPrefix );
1464  }
1465 
1487  public static function setupTestDB( IMaintainableDatabase $db, $prefix ) {
1488  if ( self::$dbSetup ) {
1489  return;
1490  }
1491 
1492  if ( $db->tablePrefix() === $prefix ) {
1493  throw new MWException(
1494  'Cannot run unit tests, the database prefix is already "' . $prefix . '"' );
1495  }
1496 
1497  // TODO: the below should be re-written as soon as LBFactory, LoadBalancer,
1498  // and Database no longer use global state.
1499 
1500  self::$dbSetup = true;
1501 
1502  if ( !self::setupDatabaseWithTestPrefix( $db, $prefix ) ) {
1503  return;
1504  }
1505 
1506  // Assuming this isn't needed for External Store database, and not sure if the procedure
1507  // would be available there.
1508  if ( $db->getType() == 'oracle' ) {
1509  $db->query( 'BEGIN FILL_WIKI_INFO; END;', __METHOD__ );
1510  }
1511 
1512  Hooks::run( 'UnitTestsAfterDatabaseSetup', [ $db, $prefix ] );
1513  }
1514 
1521  protected static function setupExternalStoreTestDBs( $testPrefix = null ) {
1522  $connections = self::getExternalStoreDatabaseConnections();
1523  foreach ( $connections as $dbw ) {
1524  self::setupDatabaseWithTestPrefix( $dbw, $testPrefix );
1525  }
1526  }
1527 
1534  protected static function getExternalStoreDatabaseConnections() {
1535  global $wgDefaultExternalStore;
1536 
1538  $externalStoreDB = ExternalStore::getStoreObject( 'DB' );
1539  $defaultArray = (array)$wgDefaultExternalStore;
1540  $dbws = [];
1541  foreach ( $defaultArray as $url ) {
1542  if ( strpos( $url, 'DB://' ) === 0 ) {
1543  list( $proto, $cluster ) = explode( '://', $url, 2 );
1544  // Avoid getMaster() because setupDatabaseWithTestPrefix()
1545  // requires Database instead of plain DBConnRef/IDatabase
1546  $dbws[] = $externalStoreDB->getMaster( $cluster );
1547  }
1548  }
1549 
1550  return $dbws;
1551  }
1552 
1558  protected static function isUsingExternalStoreDB() {
1559  global $wgDefaultExternalStore;
1560  if ( !$wgDefaultExternalStore ) {
1561  return false;
1562  }
1563 
1564  $defaultArray = (array)$wgDefaultExternalStore;
1565  foreach ( $defaultArray as $url ) {
1566  if ( strpos( $url, 'DB://' ) === 0 ) {
1567  return true;
1568  }
1569  }
1570 
1571  return false;
1572  }
1573 
1581  if ( $db->tablePrefix() !== $this->dbPrefix() ) {
1582  throw new LogicException(
1583  'Trying to delete mock tables, but table prefix does not indicate a mock database.'
1584  );
1585  }
1586  }
1587 
1588  private static $schemaOverrideDefaults = [
1589  'scripts' => [],
1590  'create' => [],
1591  'drop' => [],
1592  'alter' => [],
1593  ];
1594 
1610  return [];
1611  }
1612 
1620  private function undoSchemaOverrides( IMaintainableDatabase $db, $oldOverrides ) {
1621  $this->ensureMockDatabaseConnection( $db );
1622 
1623  $oldOverrides = $oldOverrides + self::$schemaOverrideDefaults;
1624  $originalTables = $this->listOriginalTables( $db );
1625 
1626  // Drop tables that need to be restored or removed.
1627  $tablesToDrop = array_merge( $oldOverrides['create'], $oldOverrides['alter'] );
1628 
1629  // Restore tables that have been dropped or created or altered,
1630  // if they exist in the original schema.
1631  $tablesToRestore = array_merge( $tablesToDrop, $oldOverrides['drop'] );
1632  $tablesToRestore = array_intersect( $originalTables, $tablesToRestore );
1633 
1634  if ( $tablesToDrop ) {
1635  $this->dropMockTables( $db, $tablesToDrop );
1636  }
1637 
1638  if ( $tablesToRestore ) {
1639  $this->recloneMockTables( $db, $tablesToRestore );
1640 
1641  // Reset the restored tables, mainly for the side effect of
1642  // re-calling $this->addCoreDBData() if necessary.
1643  $this->resetDB( $db, $tablesToRestore );
1644  }
1645  }
1646 
1652  private function setUpSchema( IMaintainableDatabase $db ) {
1653  // Undo any active overrides.
1654  $oldOverrides = $db->_schemaOverrides ?? self::$schemaOverrideDefaults;
1655 
1656  if ( $oldOverrides['alter'] || $oldOverrides['create'] || $oldOverrides['drop'] ) {
1657  $this->undoSchemaOverrides( $db, $oldOverrides );
1658  unset( $db->_schemaOverrides );
1659  }
1660 
1661  // Determine new overrides.
1662  $overrides = $this->getSchemaOverrides( $db ) + self::$schemaOverrideDefaults;
1663 
1664  $extraKeys = array_diff(
1665  array_keys( $overrides ),
1666  array_keys( self::$schemaOverrideDefaults )
1667  );
1668 
1669  if ( $extraKeys ) {
1670  throw new InvalidArgumentException(
1671  'Schema override contains extra keys: ' . var_export( $extraKeys, true )
1672  );
1673  }
1674 
1675  if ( !$overrides['scripts'] ) {
1676  // no scripts to run
1677  return;
1678  }
1679 
1680  if ( !$overrides['create'] && !$overrides['drop'] && !$overrides['alter'] ) {
1681  throw new InvalidArgumentException(
1682  'Schema override scripts given, but no tables are declared to be '
1683  . 'created, dropped or altered.'
1684  );
1685  }
1686 
1687  $this->ensureMockDatabaseConnection( $db );
1688 
1689  // Drop the tables that will be created by the schema scripts.
1690  $originalTables = $this->listOriginalTables( $db );
1691  $tablesToDrop = array_intersect( $originalTables, $overrides['create'] );
1692 
1693  if ( $tablesToDrop ) {
1694  $this->dropMockTables( $db, $tablesToDrop );
1695  }
1696 
1697  // Run schema override scripts.
1698  foreach ( $overrides['scripts'] as $script ) {
1699  $db->sourceFile(
1700  $script,
1701  null,
1702  null,
1703  __METHOD__,
1704  function ( $cmd ) {
1705  return $this->mungeSchemaUpdateQuery( $cmd );
1706  }
1707  );
1708  }
1709 
1710  $db->_schemaOverrides = $overrides;
1711  }
1712 
1713  private function mungeSchemaUpdateQuery( $cmd ) {
1714  return self::$useTemporaryTables
1715  ? preg_replace( '/\bCREATE\s+TABLE\b/i', 'CREATE TEMPORARY TABLE', $cmd )
1716  : $cmd;
1717  }
1718 
1726  $this->ensureMockDatabaseConnection( $db );
1727 
1728  foreach ( $tables as $tbl ) {
1729  $tbl = $db->tableName( $tbl );
1730  $db->query( "DROP TABLE IF EXISTS $tbl", __METHOD__ );
1731  }
1732  }
1733 
1741  if ( !isset( $db->_originalTablePrefix ) ) {
1742  throw new LogicException( 'No original table prefix know, cannot list tables!' );
1743  }
1744 
1745  $originalTables = $db->listTables( $db->_originalTablePrefix, __METHOD__ );
1746 
1747  $unittestPrefixRegex = '/^' . preg_quote( $this->dbPrefix(), '/' ) . '/';
1748  $originalPrefixRegex = '/^' . preg_quote( $db->_originalTablePrefix, '/' ) . '/';
1749 
1750  $originalTables = array_filter(
1751  $originalTables,
1752  function ( $pt ) use ( $unittestPrefixRegex ) {
1753  return !preg_match( $unittestPrefixRegex, $pt );
1754  }
1755  );
1756 
1757  $originalTables = array_map(
1758  function ( $pt ) use ( $originalPrefixRegex ) {
1759  return preg_replace( $originalPrefixRegex, '', $pt );
1760  },
1761  $originalTables
1762  );
1763 
1764  return array_unique( $originalTables );
1765  }
1766 
1776  $this->ensureMockDatabaseConnection( $db );
1777 
1778  if ( !isset( $db->_originalTablePrefix ) ) {
1779  throw new LogicException( 'No original table prefix know, cannot restore tables!' );
1780  }
1781 
1782  $originalTables = $this->listOriginalTables( $db );
1783  $tables = array_intersect( $tables, $originalTables );
1784 
1785  $dbClone = new CloneDatabase( $db, $tables, $db->tablePrefix(), $db->_originalTablePrefix );
1786  $dbClone->useTemporaryTables( self::$useTemporaryTables );
1787 
1788  $dbClone->cloneTableStructure();
1789  }
1790 
1797  private function resetDB( $db, $tablesUsed ) {
1798  if ( $db ) {
1799  $userTables = [ 'user', 'user_groups', 'user_properties', 'actor' ];
1800  $pageTables = [
1801  'page', 'revision', 'ip_changes', 'revision_comment_temp', 'comment', 'archive',
1802  'revision_actor_temp', 'slots', 'content', 'content_models', 'slot_roles',
1803  ];
1804  $coreDBDataTables = array_merge( $userTables, $pageTables );
1805 
1806  // If any of the user or page tables were marked as used, we should clear all of them.
1807  if ( array_intersect( $tablesUsed, $userTables ) ) {
1808  $tablesUsed = array_unique( array_merge( $tablesUsed, $userTables ) );
1810 
1811  // Reset $wgUser, which is probably 127.0.0.1, as its loaded data is probably not valid
1812  // @todo Should we start setting $wgUser to something nondeterministic
1813  // to encourage tests to be updated to not depend on it?
1814  global $wgUser;
1815  $wgUser->clearInstanceCache( $wgUser->mFrom );
1816  }
1817  if ( array_intersect( $tablesUsed, $pageTables ) ) {
1818  $tablesUsed = array_unique( array_merge( $tablesUsed, $pageTables ) );
1819  }
1820 
1821  // Postgres, Oracle, and MSSQL all use mwuser/pagecontent
1822  // instead of user/text. But Postgres does not remap the
1823  // table name in tableExists(), so we mark the real table
1824  // names as being used.
1825  if ( $db->getType() === 'postgres' ) {
1826  if ( in_array( 'user', $tablesUsed ) ) {
1827  $tablesUsed[] = 'mwuser';
1828  }
1829  if ( in_array( 'text', $tablesUsed ) ) {
1830  $tablesUsed[] = 'pagecontent';
1831  }
1832  }
1833 
1834  foreach ( $tablesUsed as $tbl ) {
1835  $this->truncateTable( $tbl, $db );
1836  }
1837 
1838  if ( array_intersect( $tablesUsed, $coreDBDataTables ) ) {
1839  // Reset services that may contain information relating to the truncated tables
1840  $this->overrideMwServices();
1841  // Re-add core DB data that was deleted
1842  $this->addCoreDBData();
1843  }
1844  }
1845  }
1846 
1855  protected function truncateTable( $tableName, IDatabase $db = null ) {
1856  if ( !$db ) {
1857  $db = $this->db;
1858  }
1859 
1860  if ( !$db->tableExists( $tableName ) ) {
1861  return;
1862  }
1863 
1864  $truncate = in_array( $db->getType(), [ 'oracle', 'mysql' ] );
1865 
1866  if ( $truncate ) {
1867  $db->query( 'TRUNCATE TABLE ' . $db->tableName( $tableName ), __METHOD__ );
1868  } else {
1869  $db->delete( $tableName, '*', __METHOD__ );
1870  }
1871 
1872  if ( $db instanceof DatabasePostgres || $db instanceof DatabaseSqlite ) {
1873  // Reset the table's sequence too.
1874  $db->resetSequenceForTable( $tableName, __METHOD__ );
1875  }
1876 
1877  // re-initialize site_stats table
1878  if ( $tableName === 'site_stats' ) {
1880  }
1881  }
1882 
1883  private static function unprefixTable( &$tableName, $ind, $prefix ) {
1884  $tableName = substr( $tableName, strlen( $prefix ) );
1885  }
1886 
1887  private static function isNotUnittest( $table ) {
1888  return strpos( $table, self::DB_PREFIX ) !== 0;
1889  }
1890 
1898  public static function listTables( IMaintainableDatabase $db ) {
1899  $prefix = $db->tablePrefix();
1900  $tables = $db->listTables( $prefix, __METHOD__ );
1901 
1902  if ( $db->getType() === 'mysql' ) {
1903  static $viewListCache = null;
1904  if ( $viewListCache === null ) {
1905  $viewListCache = $db->listViews( null, __METHOD__ );
1906  }
1907  // T45571: cannot clone VIEWs under MySQL
1908  $tables = array_diff( $tables, $viewListCache );
1909  }
1910  array_walk( $tables, [ __CLASS__, 'unprefixTable' ], $prefix );
1911 
1912  // Don't duplicate test tables from the previous fataled run
1913  $tables = array_filter( $tables, [ __CLASS__, 'isNotUnittest' ] );
1914 
1915  if ( $db->getType() == 'sqlite' ) {
1916  $tables = array_flip( $tables );
1917  // these are subtables of searchindex and don't need to be duped/dropped separately
1918  unset( $tables['searchindex_content'] );
1919  unset( $tables['searchindex_segdir'] );
1920  unset( $tables['searchindex_segments'] );
1921  $tables = array_flip( $tables );
1922  }
1923 
1924  return $tables;
1925  }
1926 
1935  public function copyTestData( IDatabase $source, IDatabase $target ) {
1936  if ( $this->db->getType() === 'sqlite' ) {
1937  // SQLite uses a non-temporary copy of the searchindex table for testing,
1938  // which gets deleted and re-created when setting up the secondary connection,
1939  // causing "Error 17" when trying to copy the data. See T191863#4130112.
1940  throw new RuntimeException(
1941  'Setting up a secondary database connection with test data is currently not'
1942  . 'with SQLite. You may want to use markTestSkippedIfDbType() to bypass this issue.'
1943  );
1944  }
1945 
1946  $tables = self::listOriginalTables( $source );
1947 
1948  foreach ( $tables as $table ) {
1949  $res = $source->select( $table, '*', [], __METHOD__ );
1950  $allRows = [];
1951 
1952  foreach ( $res as $row ) {
1953  $allRows[] = (array)$row;
1954  }
1955 
1956  $target->insert( $table, $allRows, __METHOD__, [ 'IGNORE' ] );
1957  }
1958  }
1959 
1964  protected function checkDbIsSupported() {
1965  if ( !in_array( $this->db->getType(), $this->supportedDBs ) ) {
1966  throw new MWException( $this->db->getType() . " is not currently supported for unit testing." );
1967  }
1968  }
1969 
1975  public function getCliArg( $offset ) {
1976  return $this->cliArgs[$offset] ?? null;
1977  }
1978 
1984  public function setCliArg( $offset, $value ) {
1985  $this->cliArgs[$offset] = $value;
1986  }
1987 
1995  public function hideDeprecated( $function ) {
1996  Wikimedia\suppressWarnings();
1997  wfDeprecated( $function );
1998  Wikimedia\restoreWarnings();
1999  }
2000 
2021  protected function assertSelect(
2022  $table, $fields, $condition, array $expectedRows, array $options = [], array $join_conds = []
2023  ) {
2024  if ( !$this->needsDB() ) {
2025  throw new MWException( 'When testing database state, the test cases\'s needDB()' .
2026  ' method should return true. Use @group Database or $this->tablesUsed.' );
2027  }
2028 
2029  $db = wfGetDB( DB_REPLICA );
2030 
2031  $res = $db->select(
2032  $table,
2033  $fields,
2034  $condition,
2035  wfGetCaller(),
2036  $options + [ 'ORDER BY' => $fields ],
2037  $join_conds
2038  );
2039  $this->assertNotEmpty( $res, "query failed: " . $db->lastError() );
2040 
2041  $i = 0;
2042 
2043  foreach ( $expectedRows as $expected ) {
2044  $r = $res->fetchRow();
2045  self::stripStringKeys( $r );
2046 
2047  $i += 1;
2048  $this->assertNotEmpty( $r, "row #$i missing" );
2049 
2050  $this->assertEquals( $expected, $r, "row #$i mismatches" );
2051  }
2052 
2053  $r = $res->fetchRow();
2054  self::stripStringKeys( $r );
2055 
2056  $this->assertFalse( $r, "found extra row (after #$i)" );
2057  }
2058 
2070  protected function arrayWrap( array $elements ) {
2071  return array_map(
2072  function ( $element ) {
2073  return [ $element ];
2074  },
2075  $elements
2076  );
2077  }
2078 
2091  protected function assertArrayEquals( array $expected, array $actual,
2092  $ordered = false, $named = false
2093  ) {
2094  if ( !$ordered ) {
2095  $this->objectAssociativeSort( $expected );
2096  $this->objectAssociativeSort( $actual );
2097  }
2098 
2099  if ( !$named ) {
2100  $expected = array_values( $expected );
2101  $actual = array_values( $actual );
2102  }
2103 
2104  call_user_func_array(
2105  [ $this, 'assertEquals' ],
2106  array_merge( [ $expected, $actual ], array_slice( func_get_args(), 4 ) )
2107  );
2108  }
2109 
2122  protected function assertHTMLEquals( $expected, $actual, $msg = '' ) {
2123  $expected = str_replace( '>', ">\n", $expected );
2124  $actual = str_replace( '>', ">\n", $actual );
2125 
2126  $this->assertEquals( $expected, $actual, $msg );
2127  }
2128 
2136  protected function objectAssociativeSort( array &$array ) {
2137  uasort(
2138  $array,
2139  function ( $a, $b ) {
2140  return serialize( $a ) <=> serialize( $b );
2141  }
2142  );
2143  }
2144 
2154  protected static function stripStringKeys( &$r ) {
2155  if ( !is_array( $r ) ) {
2156  return;
2157  }
2158 
2159  foreach ( $r as $k => $v ) {
2160  if ( is_string( $k ) ) {
2161  unset( $r[$k] );
2162  }
2163  }
2164  }
2165 
2179  protected function assertTypeOrValue( $type, $actual, $value = false, $message = '' ) {
2180  if ( $actual === $value ) {
2181  $this->assertTrue( true, $message );
2182  } else {
2183  $this->assertType( $type, $actual, $message );
2184  }
2185  }
2186 
2198  protected function assertType( $type, $actual, $message = '' ) {
2199  if ( class_exists( $type ) || interface_exists( $type ) ) {
2200  $this->assertInstanceOf( $type, $actual, $message );
2201  } else {
2202  $this->assertInternalType( $type, $actual, $message );
2203  }
2204  }
2205 
2215  protected function isWikitextNS( $ns ) {
2217 
2218  if ( isset( $wgNamespaceContentModels[$ns] ) ) {
2219  return $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT;
2220  }
2221 
2222  return true;
2223  }
2224 
2232  protected function getDefaultWikitextNS() {
2234 
2235  static $wikitextNS = null; // this is not going to change
2236  if ( $wikitextNS !== null ) {
2237  return $wikitextNS;
2238  }
2239 
2240  // quickly short out on most common case:
2241  if ( !isset( $wgNamespaceContentModels[NS_MAIN] ) ) {
2242  return NS_MAIN;
2243  }
2244 
2245  // NOTE: prefer content namespaces
2246  $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
2247  $namespaces = array_unique( array_merge(
2248  $nsInfo->getContentNamespaces(),
2249  [ NS_MAIN, NS_HELP, NS_PROJECT ], // prefer these
2250  $nsInfo->getValidNamespaces()
2251  ) );
2252 
2253  $namespaces = array_diff( $namespaces, [
2254  NS_FILE, NS_CATEGORY, NS_MEDIAWIKI, NS_USER // don't mess with magic namespaces
2255  ] );
2256 
2257  $talk = array_filter( $namespaces, function ( $ns ) use ( $nsInfo ) {
2258  return $nsInfo->isTalk( $ns );
2259  } );
2260 
2261  // prefer non-talk pages
2262  $namespaces = array_diff( $namespaces, $talk );
2263  $namespaces = array_merge( $namespaces, $talk );
2264 
2265  // check default content model of each namespace
2266  foreach ( $namespaces as $ns ) {
2267  if ( !isset( $wgNamespaceContentModels[$ns] ) ||
2268  $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT
2269  ) {
2270  $wikitextNS = $ns;
2271 
2272  return $wikitextNS;
2273  }
2274  }
2275 
2276  // give up
2277  // @todo Inside a test, we could skip the test as incomplete.
2278  // But frequently, this is used in fixture setup.
2279  throw new MWException( "No namespace defaults to wikitext!" );
2280  }
2281 
2288  protected function markTestSkippedIfNoDiff3() {
2289  global $wgDiff3;
2290 
2291  # This check may also protect against code injection in
2292  # case of broken installations.
2293  Wikimedia\suppressWarnings();
2294  $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 );
2295  Wikimedia\restoreWarnings();
2296 
2297  if ( !$haveDiff3 ) {
2298  $this->markTestSkipped( "Skip test, since diff3 is not configured" );
2299  }
2300  }
2301 
2310  protected function checkPHPExtension( $extName ) {
2311  $loaded = extension_loaded( $extName );
2312  if ( !$loaded ) {
2313  $this->markTestSkipped( "PHP extension '$extName' is not loaded, skipping." );
2314  }
2315 
2316  return $loaded;
2317  }
2318 
2325  protected function markTestSkippedIfDbType( $type ) {
2326  if ( $this->db->getType() === $type ) {
2327  $this->markTestSkipped( "The $type database type isn't supported for this test" );
2328  }
2329  }
2330 
2336  public static function wfResetOutputBuffersBarrier( $buffer ) {
2337  return $buffer;
2338  }
2339 
2347  protected function setTemporaryHook( $hookName, $handler ) {
2348  $this->mergeMwGlobalArrayValue( 'wgHooks', [ $hookName => [ $handler ] ] );
2349  }
2350 
2360  protected function assertFileContains(
2361  $fileName,
2362  $actualData,
2363  $createIfMissing = false,
2364  $msg = ''
2365  ) {
2366  if ( $createIfMissing ) {
2367  if ( !file_exists( $fileName ) ) {
2368  file_put_contents( $fileName, $actualData );
2369  $this->markTestSkipped( "Data file $fileName does not exist" );
2370  }
2371  } else {
2372  self::assertFileExists( $fileName );
2373  }
2374  self::assertEquals( file_get_contents( $fileName ), $actualData, $msg );
2375  }
2376 
2389  protected function editPage( $pageName, $text, $summary = '', $defaultNs = NS_MAIN ) {
2390  if ( !$this->needsDB() ) {
2391  throw new MWException( 'When testing which pages, the test cases\'s needsDB()' .
2392  ' method should return true. Use @group Database or $this->tablesUsed.' );
2393  }
2394 
2395  $title = Title::newFromText( $pageName, $defaultNs );
2396  $page = WikiPage::factory( $title );
2397 
2398  return $page->doEditContent( ContentHandler::makeContent( $text, $title ), $summary );
2399  }
2400 
2409  protected function revisionDelete(
2410  $rev, array $value = [ Revision::DELETED_TEXT => 1 ], $comment = ''
2411  ) {
2412  if ( is_int( $rev ) ) {
2414  }
2416  'revision', RequestContext::getMain(), $rev->getTitle(), [ $rev->getId() ]
2417  )->setVisibility( [
2418  'value' => $value,
2419  'comment' => $comment,
2420  ] );
2421  }
2422 
2431  protected function anythingBut( ...$values ) {
2432  return $this->logicalNot( $this->logicalOr(
2433  ...array_map( [ $this, 'matches' ], $values )
2434  ) );
2435  }
2436 }
static setupTestDB(IMaintainableDatabase $db, $prefix)
Creates an empty skeleton of the wiki database by cloning its structure to equivalent tables using th...
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)
static resetGlobalParser()
If $wgParser has been unstubbed, replace it with a fresh one so it picks up any config changes...
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)
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:2159
doStashMwGlobals( $globalKeys)
$wgParser
Definition: Setup.php:939
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.
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
mergeMwGlobalArrayValue( $name, $values)
Merges the given values into a MW global array variable.
static resetIdByNameCache()
Reset the cache used in idFromName().
Definition: User.php:960
MediaWikiServices $localServices
The local service locator, created during setUp().
query( $sql, $fname=__METHOD__, $flags=0)
Run an SQL query and return the result.
Definition: Database.php:1192
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:116
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
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:1980
$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:50
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...
anythingBut(... $values)
Returns a PHPUnit constraint that matches anything other than a fixed set of values.
resetNamespaces()
Must be called whenever namespaces are changed, e.g., $wgExtraNamespaces is altered.
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 '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:1263
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:2217
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:2376
static getExternalStoreDatabaseConnections()
Gets master database connections for all of the ExternalStoreDB stores configured in $wgDefaultExtern...
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:1982
static resetMain()
Resets singleton returned by getMain().
static getMutableTestUser( $testName, $groups=[])
Get a TestUser object that the caller may modify.
unserialize( $serialized)
Class to implement stub globals, which are globals that delay loading the their associated module cod...
Definition: StubObject.php:45
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:1766
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 resetGetDefaultOptionsForTestsOnly()
Reset the process cache of default user options.
Definition: User.php:1737
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().
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
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
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
Definition: Database.php:1777
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.
__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:2981
static unprefixTable(&$tableName, $ind, $prefix)
static clearCaches()
Intended for tests that may change configuration in a way that invalidates caches.
Definition: Language.php:285
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:780
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.
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
query( $sql, $fname=__METHOD__, $flags=0)
Run an SQL query and return the result.
$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
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:1473
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:319