MediaWiki  1.28.1
MediaWikiTestCase.php
Go to the documentation of this file.
1 <?php
7 
11 abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
12 
20  private static $serviceLocator = null;
21 
34  private $called = [];
35 
40  public static $users;
41 
48  protected $db;
49 
54  protected $tablesUsed = []; // tables with data
55 
56  private static $useTemporaryTables = true;
57  private static $reuseDB = false;
58  private static $dbSetup = false;
59  private static $oldTablePrefix = '';
60 
66  private $phpErrorLevel;
67 
74  private $tmpFiles = [];
75 
82  private $mwGlobals = [];
83 
88  private $loggers = [];
89 
93  const DB_PREFIX = 'unittest_';
94  const ORA_DB_PREFIX = 'ut_';
95 
100  protected $supportedDBs = [
101  'mysql',
102  'sqlite',
103  'postgres',
104  'oracle'
105  ];
106 
107  public function __construct( $name = null, array $data = [], $dataName = '' ) {
108  parent::__construct( $name, $data, $dataName );
109 
110  $this->backupGlobals = false;
111  $this->backupStaticAttributes = false;
112  }
113 
114  public function __destruct() {
115  // Complain if self::setUp() was called, but not self::tearDown()
116  // $this->called['setUp'] will be checked by self::testMediaWikiTestCaseParentSetupCalled()
117  if ( isset( $this->called['setUp'] ) && !isset( $this->called['tearDown'] ) ) {
118  throw new MWException( static::class . "::tearDown() must call parent::tearDown()" );
119  }
120  }
121 
122  public static function setUpBeforeClass() {
123  parent::setUpBeforeClass();
124 
125  // Get the service locator, and reset services if it's not done already
126  self::$serviceLocator = self::prepareServices( new GlobalVarConfig() );
127  }
128 
137  public static function getTestUser( $groups = [] ) {
138  return TestUserRegistry::getImmutableTestUser( $groups );
139  }
140 
149  public static function getMutableTestUser( $groups = [] ) {
150  return TestUserRegistry::getMutableTestUser( __CLASS__, $groups );
151  }
152 
161  public static function getTestSysop() {
162  return self::getTestUser( [ 'sysop', 'bureaucrat' ] );
163  }
164 
184  public static function prepareServices( Config $bootstrapConfig ) {
185  static $services = null;
186 
187  if ( !$services ) {
188  $services = self::resetGlobalServices( $bootstrapConfig );
189  }
190  return $services;
191  }
192 
206  protected static function resetGlobalServices( Config $bootstrapConfig = null ) {
207  $oldServices = MediaWikiServices::getInstance();
208  $oldConfigFactory = $oldServices->getConfigFactory();
209 
210  $testConfig = self::makeTestConfig( $bootstrapConfig );
211 
212  MediaWikiServices::resetGlobalInstance( $testConfig );
213 
214  $serviceLocator = MediaWikiServices::getInstance();
215  self::installTestServices(
216  $oldConfigFactory,
218  );
219  return $serviceLocator;
220  }
221 
231  private static function makeTestConfig(
232  Config $baseConfig = null,
233  Config $customOverrides = null
234  ) {
235  $defaultOverrides = new HashConfig();
236 
237  if ( !$baseConfig ) {
238  $baseConfig = MediaWikiServices::getInstance()->getBootstrapConfig();
239  }
240 
241  /* Some functions require some kind of caching, and will end up using the db,
242  * which we can't allow, as that would open a new connection for mysql.
243  * Replace with a HashBag. They would not be going to persist anyway.
244  */
245  $hashCache = [ 'class' => 'HashBagOStuff', 'reportDupes' => false ];
246  $objectCaches = [
247  CACHE_DB => $hashCache,
248  CACHE_ACCEL => $hashCache,
249  CACHE_MEMCACHED => $hashCache,
250  'apc' => $hashCache,
251  'xcache' => $hashCache,
252  'wincache' => $hashCache,
253  ] + $baseConfig->get( 'ObjectCaches' );
254 
255  $defaultOverrides->set( 'ObjectCaches', $objectCaches );
256  $defaultOverrides->set( 'MainCacheType', CACHE_NONE );
257  $defaultOverrides->set( 'JobTypeConf', [ 'default' => [ 'class' => 'JobQueueMemory' ] ] );
258 
259  // Use a fast hash algorithm to hash passwords.
260  $defaultOverrides->set( 'PasswordDefault', 'A' );
261 
262  $testConfig = $customOverrides
263  ? new MultiConfig( [ $customOverrides, $defaultOverrides, $baseConfig ] )
264  : new MultiConfig( [ $defaultOverrides, $baseConfig ] );
265 
266  return $testConfig;
267  }
268 
275  private static function installTestServices(
276  ConfigFactory $oldConfigFactory,
277  MediaWikiServices $newServices
278  ) {
279  // Use bootstrap config for all configuration.
280  // This allows config overrides via global variables to take effect.
281  $bootstrapConfig = $newServices->getBootstrapConfig();
282  $newServices->resetServiceForTesting( 'ConfigFactory' );
283  $newServices->redefineService(
284  'ConfigFactory',
285  self::makeTestConfigFactoryInstantiator(
286  $oldConfigFactory,
287  [ 'main' => $bootstrapConfig ]
288  )
289  );
290  }
291 
298  private static function makeTestConfigFactoryInstantiator(
299  ConfigFactory $oldFactory,
300  array $configurations
301  ) {
302  return function( MediaWikiServices $services ) use ( $oldFactory, $configurations ) {
303  $factory = new ConfigFactory();
304 
305  // clone configurations from $oldFactory that are not overwritten by $configurations
306  $namesToClone = array_diff(
307  $oldFactory->getConfigNames(),
308  array_keys( $configurations )
309  );
310 
311  foreach ( $namesToClone as $name ) {
312  $factory->register( $name, $oldFactory->makeConfig( $name ) );
313  }
314 
315  foreach ( $configurations as $name => $config ) {
316  $factory->register( $name, $config );
317  }
318 
319  return $factory;
320  };
321  }
322 
334  private function doLightweightServiceReset() {
336 
339  $services = MediaWikiServices::getInstance();
340  $services->resetServiceForTesting( 'MainObjectStash' );
341  $services->resetServiceForTesting( 'LocalServerObjectCache' );
342  $services->getMainWANObjectCache()->clearProcessCache();
344 
345  // TODO: move global state into MediaWikiServices
347  if ( session_id() !== '' ) {
348  session_write_close();
349  session_id( '' );
350  }
351 
352  $wgRequest = new FauxRequest();
354  }
355 
356  public function run( PHPUnit_Framework_TestResult $result = null ) {
357  // Reset all caches between tests.
358  $this->doLightweightServiceReset();
359 
360  $needsResetDB = false;
361 
362  if ( !self::$dbSetup || $this->needsDB() ) {
363  // set up a DB connection for this test to use
364 
365  self::$useTemporaryTables = !$this->getCliArg( 'use-normal-tables' );
366  self::$reuseDB = $this->getCliArg( 'reuse-db' );
367 
368  $this->db = wfGetDB( DB_MASTER );
369 
370  $this->checkDbIsSupported();
371 
372  if ( !self::$dbSetup ) {
373  $this->setupAllTestDBs();
374  $this->addCoreDBData();
375 
376  if ( ( $this->db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
377  $this->resetDB( $this->db, $this->tablesUsed );
378  }
379  }
380 
381  // TODO: the DB setup should be done in setUpBeforeClass(), so the test DB
382  // is available in subclass's setUpBeforeClass() and setUp() methods.
383  // This would also remove the need for the HACK that is oncePerClass().
384  if ( $this->oncePerClass() ) {
385  $this->addDBDataOnce();
386  }
387 
388  $this->addDBData();
389  $needsResetDB = true;
390  }
391 
392  parent::run( $result );
393 
394  if ( $needsResetDB ) {
395  $this->resetDB( $this->db, $this->tablesUsed );
396  }
397  }
398 
402  private function oncePerClass() {
403  // Remember current test class in the database connection,
404  // so we know when we need to run addData.
405 
406  $class = static::class;
407 
408  $first = !isset( $this->db->_hasDataForTestClass )
409  || $this->db->_hasDataForTestClass !== $class;
410 
411  $this->db->_hasDataForTestClass = $class;
412  return $first;
413  }
414 
420  public function usesTemporaryTables() {
421  return self::$useTemporaryTables;
422  }
423 
433  protected function getNewTempFile() {
434  $fileName = tempnam( wfTempDir(), 'MW_PHPUnit_' . get_class( $this ) . '_' );
435  $this->tmpFiles[] = $fileName;
436 
437  return $fileName;
438  }
439 
450  protected function getNewTempDirectory() {
451  // Starting of with a temporary /file/.
452  $fileName = $this->getNewTempFile();
453 
454  // Converting the temporary /file/ to a /directory/
455  // The following is not atomic, but at least we now have a single place,
456  // where temporary directory creation is bundled and can be improved
457  unlink( $fileName );
458  $this->assertTrue( wfMkdirParents( $fileName ) );
459 
460  return $fileName;
461  }
462 
463  protected function setUp() {
464  parent::setUp();
465  $this->called['setUp'] = true;
466 
467  $this->phpErrorLevel = intval( ini_get( 'error_reporting' ) );
468 
469  // Cleaning up temporary files
470  foreach ( $this->tmpFiles as $fileName ) {
471  if ( is_file( $fileName ) || ( is_link( $fileName ) ) ) {
472  unlink( $fileName );
473  } elseif ( is_dir( $fileName ) ) {
474  wfRecursiveRemoveDir( $fileName );
475  }
476  }
477 
478  if ( $this->needsDB() && $this->db ) {
479  // Clean up open transactions
480  while ( $this->db->trxLevel() > 0 ) {
481  $this->db->rollback( __METHOD__, 'flush' );
482  }
483  // Check for unsafe queries
484  if ( $this->db->getType() === 'mysql' ) {
485  $this->db->query( "SET sql_mode = 'STRICT_ALL_TABLES'" );
486  }
487  }
488 
490  ObjectCache::getMainWANInstance()->clearProcessCache();
491 
492  // XXX: reset maintenance triggers
493  // Hook into period lag checks which often happen in long-running scripts
494  $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
496 
497  ob_start( 'MediaWikiTestCase::wfResetOutputBuffersBarrier' );
498  }
499 
500  protected function addTmpFiles( $files ) {
501  $this->tmpFiles = array_merge( $this->tmpFiles, (array)$files );
502  }
503 
504  protected function tearDown() {
506 
507  $status = ob_get_status();
508  if ( isset( $status['name'] ) &&
509  $status['name'] === 'MediaWikiTestCase::wfResetOutputBuffersBarrier'
510  ) {
511  ob_end_flush();
512  }
513 
514  $this->called['tearDown'] = true;
515  // Cleaning up temporary files
516  foreach ( $this->tmpFiles as $fileName ) {
517  if ( is_file( $fileName ) || ( is_link( $fileName ) ) ) {
518  unlink( $fileName );
519  } elseif ( is_dir( $fileName ) ) {
520  wfRecursiveRemoveDir( $fileName );
521  }
522  }
523 
524  if ( $this->needsDB() && $this->db ) {
525  // Clean up open transactions
526  while ( $this->db->trxLevel() > 0 ) {
527  $this->db->rollback( __METHOD__, 'flush' );
528  }
529  if ( $this->db->getType() === 'mysql' ) {
530  $this->db->query( "SET sql_mode = " . $this->db->addQuotes( $wgSQLMode ) );
531  }
532  }
533 
534  // Restore mw globals
535  foreach ( $this->mwGlobals as $key => $value ) {
536  $GLOBALS[$key] = $value;
537  }
538  $this->mwGlobals = [];
539  $this->restoreLoggers();
540 
541  if ( self::$serviceLocator && MediaWikiServices::getInstance() !== self::$serviceLocator ) {
542  MediaWikiServices::forceGlobalInstance( self::$serviceLocator );
543  }
544 
545  // TODO: move global state into MediaWikiServices
547  if ( session_id() !== '' ) {
548  session_write_close();
549  session_id( '' );
550  }
551  $wgRequest = new FauxRequest();
554 
555  $phpErrorLevel = intval( ini_get( 'error_reporting' ) );
556 
557  if ( $phpErrorLevel !== $this->phpErrorLevel ) {
558  ini_set( 'error_reporting', $this->phpErrorLevel );
559 
560  $oldHex = strtoupper( dechex( $this->phpErrorLevel ) );
561  $newHex = strtoupper( dechex( $phpErrorLevel ) );
562  $message = "PHP error_reporting setting was left dirty: "
563  . "was 0x$oldHex before test, 0x$newHex after test!";
564 
565  $this->fail( $message );
566  }
567 
568  parent::tearDown();
569  }
570 
575  final public function testMediaWikiTestCaseParentSetupCalled() {
576  $this->assertArrayHasKey( 'setUp', $this->called,
577  static::class . '::setUp() must call parent::setUp()'
578  );
579  }
580 
590  protected function setService( $name, $object ) {
591  // If we did not yet override the service locator, so so now.
592  if ( MediaWikiServices::getInstance() === self::$serviceLocator ) {
593  $this->overrideMwServices();
594  }
595 
596  MediaWikiServices::getInstance()->disableService( $name );
597  MediaWikiServices::getInstance()->redefineService(
598  $name,
599  function () use ( $object ) {
600  return $object;
601  }
602  );
603  }
604 
640  protected function setMwGlobals( $pairs, $value = null ) {
641  if ( is_string( $pairs ) ) {
642  $pairs = [ $pairs => $value ];
643  }
644 
645  $this->stashMwGlobals( array_keys( $pairs ) );
646 
647  foreach ( $pairs as $key => $value ) {
648  $GLOBALS[$key] = $value;
649  }
650  }
651 
660  private static function canShallowCopy( $value ) {
661  if ( is_scalar( $value ) || $value === null ) {
662  return true;
663  }
664  if ( is_array( $value ) ) {
665  foreach ( $value as $subValue ) {
666  if ( !is_scalar( $subValue ) && $subValue !== null ) {
667  return false;
668  }
669  }
670  return true;
671  }
672  return false;
673  }
674 
694  protected function stashMwGlobals( $globalKeys ) {
695  if ( is_string( $globalKeys ) ) {
696  $globalKeys = [ $globalKeys ];
697  }
698 
699  foreach ( $globalKeys as $globalKey ) {
700  // NOTE: make sure we only save the global once or a second call to
701  // setMwGlobals() on the same global would override the original
702  // value.
703  if ( !array_key_exists( $globalKey, $this->mwGlobals ) ) {
704  if ( !array_key_exists( $globalKey, $GLOBALS ) ) {
705  throw new Exception( "Global with key {$globalKey} doesn't exist and cant be stashed" );
706  }
707  // NOTE: we serialize then unserialize the value in case it is an object
708  // this stops any objects being passed by reference. We could use clone
709  // and if is_object but this does account for objects within objects!
710  if ( self::canShallowCopy( $GLOBALS[$globalKey] ) ) {
711  $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
712  } elseif (
713  // Many MediaWiki types are safe to clone. These are the
714  // ones that are most commonly stashed.
715  $GLOBALS[$globalKey] instanceof Language ||
716  $GLOBALS[$globalKey] instanceof User ||
717  $GLOBALS[$globalKey] instanceof FauxRequest
718  ) {
719  $this->mwGlobals[$globalKey] = clone $GLOBALS[$globalKey];
720  } else {
721  try {
722  $this->mwGlobals[$globalKey] = unserialize( serialize( $GLOBALS[$globalKey] ) );
723  } catch ( Exception $e ) {
724  $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
725  }
726  }
727  }
728  }
729  }
730 
746  protected function mergeMwGlobalArrayValue( $name, $values ) {
747  if ( !isset( $GLOBALS[$name] ) ) {
748  $merged = $values;
749  } else {
750  if ( !is_array( $GLOBALS[$name] ) ) {
751  throw new MWException( "MW global $name is not an array." );
752  }
753 
754  // NOTE: do not use array_merge, it screws up for numeric keys.
755  $merged = $GLOBALS[$name];
756  foreach ( $values as $k => $v ) {
757  $merged[$k] = $v;
758  }
759  }
760 
761  $this->setMwGlobals( $name, $merged );
762  }
763 
778  protected function overrideMwServices( Config $configOverrides = null, array $services = [] ) {
779  if ( !$configOverrides ) {
780  $configOverrides = new HashConfig();
781  }
782 
783  $oldInstance = MediaWikiServices::getInstance();
784  $oldConfigFactory = $oldInstance->getConfigFactory();
785 
786  $testConfig = self::makeTestConfig( null, $configOverrides );
787  $newInstance = new MediaWikiServices( $testConfig );
788 
789  // Load the default wiring from the specified files.
790  // NOTE: this logic mirrors the logic in MediaWikiServices::newInstance.
791  $wiringFiles = $testConfig->get( 'ServiceWiringFiles' );
792  $newInstance->loadWiringFiles( $wiringFiles );
793 
794  // Provide a traditional hook point to allow extensions to configure services.
795  Hooks::run( 'MediaWikiServices', [ $newInstance ] );
796 
797  foreach ( $services as $name => $callback ) {
798  $newInstance->redefineService( $name, $callback );
799  }
800 
801  self::installTestServices(
802  $oldConfigFactory,
803  $newInstance
804  );
805  MediaWikiServices::forceGlobalInstance( $newInstance );
806 
807  return $newInstance;
808  }
809 
814  public function setUserLang( $lang ) {
815  RequestContext::getMain()->setLanguage( $lang );
816  $this->setMwGlobals( 'wgLang', RequestContext::getMain()->getLanguage() );
817  }
818 
823  public function setContentLang( $lang ) {
824  if ( $lang instanceof Language ) {
825  $langCode = $lang->getCode();
826  $langObj = $lang;
827  } else {
828  $langCode = $lang;
829  $langObj = Language::factory( $langCode );
830  }
831  $this->setMwGlobals( [
832  'wgLanguageCode' => $langCode,
833  'wgContLang' => $langObj,
834  ] );
835  }
836 
843  protected function setLogger( $channel, LoggerInterface $logger ) {
844  // TODO: Once loggers are managed by MediaWikiServices, use
845  // overrideMwServices() to set loggers.
846 
847  $provider = LoggerFactory::getProvider();
848  $wrappedProvider = TestingAccessWrapper::newFromObject( $provider );
849  $singletons = $wrappedProvider->singletons;
850  if ( $provider instanceof MonologSpi ) {
851  if ( !isset( $this->loggers[$channel] ) ) {
852  $this->loggers[$channel] = isset( $singletons['loggers'][$channel] )
853  ? $singletons['loggers'][$channel] : null;
854  }
855  $singletons['loggers'][$channel] = $logger;
856  } elseif ( $provider instanceof LegacySpi ) {
857  if ( !isset( $this->loggers[$channel] ) ) {
858  $this->loggers[$channel] = isset( $singletons[$channel] ) ? $singletons[$channel] : null;
859  }
860  $singletons[$channel] = $logger;
861  } else {
862  throw new LogicException( __METHOD__ . ': setting a logger for ' . get_class( $provider )
863  . ' is not implemented' );
864  }
865  $wrappedProvider->singletons = $singletons;
866  }
867 
872  private function restoreLoggers() {
873  $provider = LoggerFactory::getProvider();
874  $wrappedProvider = TestingAccessWrapper::newFromObject( $provider );
875  $singletons = $wrappedProvider->singletons;
876  foreach ( $this->loggers as $channel => $logger ) {
877  if ( $provider instanceof MonologSpi ) {
878  if ( $logger === null ) {
879  unset( $singletons['loggers'][$channel] );
880  } else {
881  $singletons['loggers'][$channel] = $logger;
882  }
883  } elseif ( $provider instanceof LegacySpi ) {
884  if ( $logger === null ) {
885  unset( $singletons[$channel] );
886  } else {
887  $singletons[$channel] = $logger;
888  }
889  }
890  }
891  $wrappedProvider->singletons = $singletons;
892  $this->loggers = [];
893  }
894 
899  public function dbPrefix() {
900  return $this->db->getType() == 'oracle' ? self::ORA_DB_PREFIX : self::DB_PREFIX;
901  }
902 
907  public function needsDB() {
908  # if the test says it uses database tables, it needs the database
909  if ( $this->tablesUsed ) {
910  return true;
911  }
912 
913  # if the test says it belongs to the Database group, it needs the database
914  $rc = new ReflectionClass( $this );
915  if ( preg_match( '/@group +Database/im', $rc->getDocComment() ) ) {
916  return true;
917  }
918 
919  return false;
920  }
921 
933  protected function insertPage(
934  $pageName,
935  $text = 'Sample page for unit test.',
936  $namespace = null
937  ) {
938  if ( is_string( $pageName ) ) {
939  $title = Title::newFromText( $pageName, $namespace );
940  } else {
941  $title = $pageName;
942  }
943 
944  $user = static::getTestSysop()->getUser();
945  $comment = __METHOD__ . ': Sample page for unit test.';
946 
947  // Avoid memory leak...?
948  // LinkCache::singleton()->clear();
949  // Maybe. But doing this absolutely breaks $title->isRedirect() when called during unit tests....
950 
952  $page->doEditContent( ContentHandler::makeContent( $text, $title ), $comment, 0, false, $user );
953 
954  return [
955  'title' => $title,
956  'id' => $page->getId(),
957  ];
958  }
959 
975  public function addDBDataOnce() {
976  }
977 
987  public function addDBData() {
988  }
989 
990  private function addCoreDBData() {
991  if ( $this->db->getType() == 'oracle' ) {
992 
993  # Insert 0 user to prevent FK violations
994  # Anonymous user
995  if ( !$this->db->selectField( 'user', '1', [ 'user_id' => 0 ] ) ) {
996  $this->db->insert( 'user', [
997  'user_id' => 0,
998  'user_name' => 'Anonymous' ], __METHOD__, [ 'IGNORE' ] );
999  }
1000 
1001  # Insert 0 page to prevent FK violations
1002  # Blank page
1003  if ( !$this->db->selectField( 'page', '1', [ 'page_id' => 0 ] ) ) {
1004  $this->db->insert( 'page', [
1005  'page_id' => 0,
1006  'page_namespace' => 0,
1007  'page_title' => ' ',
1008  'page_restrictions' => null,
1009  'page_is_redirect' => 0,
1010  'page_is_new' => 0,
1011  'page_random' => 0,
1012  'page_touched' => $this->db->timestamp(),
1013  'page_latest' => 0,
1014  'page_len' => 0 ], __METHOD__, [ 'IGNORE' ] );
1015  }
1016  }
1017 
1019 
1020  // Make sysop user
1021  $user = static::getTestSysop()->getUser();
1022 
1023  // Make 1 page with 1 revision
1024  $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
1025  if ( $page->getId() == 0 ) {
1026  $page->doEditContent(
1027  new WikitextContent( 'UTContent' ),
1028  'UTPageSummary',
1029  EDIT_NEW,
1030  false,
1031  $user
1032  );
1033 
1034  // doEditContent() probably started the session via
1035  // User::loadFromSession(). Close it now.
1036  if ( session_id() !== '' ) {
1037  session_write_close();
1038  session_id( '' );
1039  }
1040  }
1041  }
1042 
1050  public static function teardownTestDB() {
1051  global $wgJobClasses;
1052 
1053  if ( !self::$dbSetup ) {
1054  return;
1055  }
1056 
1057  foreach ( $wgJobClasses as $type => $class ) {
1058  // Delete any jobs under the clone DB (or old prefix in other stores)
1059  JobQueueGroup::singleton()->get( $type )->delete();
1060  }
1061 
1062  CloneDatabase::changePrefix( self::$oldTablePrefix );
1063 
1064  self::$oldTablePrefix = false;
1065  self::$dbSetup = false;
1066  }
1067 
1081  protected static function setupDatabaseWithTestPrefix( Database $db, $prefix ) {
1082  $tablesCloned = self::listTables( $db );
1083  $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix );
1084  $dbClone->useTemporaryTables( self::$useTemporaryTables );
1085 
1086  if ( ( $db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
1087  CloneDatabase::changePrefix( $prefix );
1088 
1089  return false;
1090  } else {
1091  $dbClone->cloneTableStructure();
1092  return true;
1093  }
1094  }
1095 
1099  public function setupAllTestDBs() {
1101 
1102  self::$oldTablePrefix = $wgDBprefix;
1103 
1104  $testPrefix = $this->dbPrefix();
1105 
1106  // switch to a temporary clone of the database
1107  self::setupTestDB( $this->db, $testPrefix );
1108 
1109  if ( self::isUsingExternalStoreDB() ) {
1110  self::setupExternalStoreTestDBs( $testPrefix );
1111  }
1112  }
1113 
1135  public static function setupTestDB( Database $db, $prefix ) {
1136  if ( self::$dbSetup ) {
1137  return;
1138  }
1139 
1140  if ( $db->tablePrefix() === $prefix ) {
1141  throw new MWException(
1142  'Cannot run unit tests, the database prefix is already "' . $prefix . '"' );
1143  }
1144 
1145  // TODO: the below should be re-written as soon as LBFactory, LoadBalancer,
1146  // and Database no longer use global state.
1147 
1148  self::$dbSetup = true;
1149 
1150  if ( !self::setupDatabaseWithTestPrefix( $db, $prefix ) ) {
1151  return;
1152  }
1153 
1154  // Assuming this isn't needed for External Store database, and not sure if the procedure
1155  // would be available there.
1156  if ( $db->getType() == 'oracle' ) {
1157  $db->query( 'BEGIN FILL_WIKI_INFO; END;' );
1158  }
1159  }
1160 
1166  protected static function setupExternalStoreTestDBs( $testPrefix ) {
1167  $connections = self::getExternalStoreDatabaseConnections();
1168  foreach ( $connections as $dbw ) {
1169  // Hack: cloneTableStructure sets $wgDBprefix to the unit test
1170  // prefix,. Even though listTables now uses tablePrefix, that
1171  // itself is populated from $wgDBprefix by default.
1172 
1173  // We have to set it back, or we won't find the original 'blobs'
1174  // table to copy.
1175 
1176  $dbw->tablePrefix( self::$oldTablePrefix );
1177  self::setupDatabaseWithTestPrefix( $dbw, $testPrefix );
1178  }
1179  }
1180 
1188  protected static function getExternalStoreDatabaseConnections() {
1190 
1192  $externalStoreDB = ExternalStore::getStoreObject( 'DB' );
1193  $defaultArray = (array) $wgDefaultExternalStore;
1194  $dbws = [];
1195  foreach ( $defaultArray as $url ) {
1196  if ( strpos( $url, 'DB://' ) === 0 ) {
1197  list( $proto, $cluster ) = explode( '://', $url, 2 );
1198  // Avoid getMaster() because setupDatabaseWithTestPrefix()
1199  // requires Database instead of plain DBConnRef/IDatabase
1200  $lb = $externalStoreDB->getLoadBalancer( $cluster );
1201  $dbw = $lb->getConnection( DB_MASTER );
1202  $dbws[] = $dbw;
1203  }
1204  }
1205 
1206  return $dbws;
1207  }
1208 
1214  protected static function isUsingExternalStoreDB() {
1216  if ( !$wgDefaultExternalStore ) {
1217  return false;
1218  }
1219 
1220  $defaultArray = (array) $wgDefaultExternalStore;
1221  foreach ( $defaultArray as $url ) {
1222  if ( strpos( $url, 'DB://' ) === 0 ) {
1223  return true;
1224  }
1225  }
1226 
1227  return false;
1228  }
1229 
1236  private function resetDB( $db, $tablesUsed ) {
1237  if ( $db ) {
1238  $userTables = [ 'user', 'user_groups', 'user_properties' ];
1239  $coreDBDataTables = array_merge( $userTables, [ 'page', 'revision' ] );
1240 
1241  // If any of the user tables were marked as used, we should clear all of them.
1242  if ( array_intersect( $tablesUsed, $userTables ) ) {
1243  $tablesUsed = array_unique( array_merge( $tablesUsed, $userTables ) );
1245  }
1246 
1247  $truncate = in_array( $db->getType(), [ 'oracle', 'mysql' ] );
1248  foreach ( $tablesUsed as $tbl ) {
1249  // TODO: reset interwiki table to its original content.
1250  if ( $tbl == 'interwiki' ) {
1251  continue;
1252  }
1253 
1254  if ( $truncate ) {
1255  $db->query( 'TRUNCATE TABLE ' . $db->tableName( $tbl ), __METHOD__ );
1256  } else {
1257  $db->delete( $tbl, '*', __METHOD__ );
1258  }
1259 
1260  if ( $tbl === 'page' ) {
1261  // Forget about the pages since they don't
1262  // exist in the DB.
1263  LinkCache::singleton()->clear();
1264  }
1265  }
1266 
1267  if ( array_intersect( $tablesUsed, $coreDBDataTables ) ) {
1268  // Re-add core DB data that was deleted
1269  $this->addCoreDBData();
1270  }
1271  }
1272  }
1273 
1283  public function __call( $func, $args ) {
1284  static $compatibility = [
1285  'assertEmpty' => 'assertEmpty2', // assertEmpty was added in phpunit 3.7.32
1286  ];
1287 
1288  if ( isset( $compatibility[$func] ) ) {
1289  return call_user_func_array( [ $this, $compatibility[$func] ], $args );
1290  } else {
1291  throw new MWException( "Called non-existent $func method on "
1292  . get_class( $this ) );
1293  }
1294  }
1295 
1301  private function assertEmpty2( $value, $msg ) {
1302  $this->assertTrue( $value == '', $msg );
1303  }
1304 
1305  private static function unprefixTable( &$tableName, $ind, $prefix ) {
1306  $tableName = substr( $tableName, strlen( $prefix ) );
1307  }
1308 
1309  private static function isNotUnittest( $table ) {
1310  return strpos( $table, 'unittest_' ) !== 0;
1311  }
1312 
1320  public static function listTables( Database $db ) {
1321  $prefix = $db->tablePrefix();
1322  $tables = $db->listTables( $prefix, __METHOD__ );
1323 
1324  if ( $db->getType() === 'mysql' ) {
1325  static $viewListCache = null;
1326  if ( $viewListCache === null ) {
1327  $viewListCache = $db->listViews( null, __METHOD__ );
1328  }
1329  // T45571: cannot clone VIEWs under MySQL
1330  $tables = array_diff( $tables, $viewListCache );
1331  }
1332  array_walk( $tables, [ __CLASS__, 'unprefixTable' ], $prefix );
1333 
1334  // Don't duplicate test tables from the previous fataled run
1335  $tables = array_filter( $tables, [ __CLASS__, 'isNotUnittest' ] );
1336 
1337  if ( $db->getType() == 'sqlite' ) {
1338  $tables = array_flip( $tables );
1339  // these are subtables of searchindex and don't need to be duped/dropped separately
1340  unset( $tables['searchindex_content'] );
1341  unset( $tables['searchindex_segdir'] );
1342  unset( $tables['searchindex_segments'] );
1343  $tables = array_flip( $tables );
1344  }
1345 
1346  return $tables;
1347  }
1348 
1353  protected function checkDbIsSupported() {
1354  if ( !in_array( $this->db->getType(), $this->supportedDBs ) ) {
1355  throw new MWException( $this->db->getType() . " is not currently supported for unit testing." );
1356  }
1357  }
1358 
1364  public function getCliArg( $offset ) {
1365  if ( isset( PHPUnitMaintClass::$additionalOptions[$offset] ) ) {
1366  return PHPUnitMaintClass::$additionalOptions[$offset];
1367  }
1368  }
1369 
1375  public function setCliArg( $offset, $value ) {
1377  }
1378 
1386  public function hideDeprecated( $function ) {
1387  MediaWiki\suppressWarnings();
1388  wfDeprecated( $function );
1389  MediaWiki\restoreWarnings();
1390  }
1391 
1410  protected function assertSelect( $table, $fields, $condition, array $expectedRows ) {
1411  if ( !$this->needsDB() ) {
1412  throw new MWException( 'When testing database state, the test cases\'s needDB()' .
1413  ' method should return true. Use @group Database or $this->tablesUsed.' );
1414  }
1415 
1416  $db = wfGetDB( DB_SLAVE );
1417 
1418  $res = $db->select( $table, $fields, $condition, wfGetCaller(), [ 'ORDER BY' => $fields ] );
1419  $this->assertNotEmpty( $res, "query failed: " . $db->lastError() );
1420 
1421  $i = 0;
1422 
1423  foreach ( $expectedRows as $expected ) {
1424  $r = $res->fetchRow();
1425  self::stripStringKeys( $r );
1426 
1427  $i += 1;
1428  $this->assertNotEmpty( $r, "row #$i missing" );
1429 
1430  $this->assertEquals( $expected, $r, "row #$i mismatches" );
1431  }
1432 
1433  $r = $res->fetchRow();
1434  self::stripStringKeys( $r );
1435 
1436  $this->assertFalse( $r, "found extra row (after #$i)" );
1437  }
1438 
1450  protected function arrayWrap( array $elements ) {
1451  return array_map(
1452  function ( $element ) {
1453  return [ $element ];
1454  },
1455  $elements
1456  );
1457  }
1458 
1471  protected function assertArrayEquals( array $expected, array $actual,
1472  $ordered = false, $named = false
1473  ) {
1474  if ( !$ordered ) {
1475  $this->objectAssociativeSort( $expected );
1476  $this->objectAssociativeSort( $actual );
1477  }
1478 
1479  if ( !$named ) {
1480  $expected = array_values( $expected );
1481  $actual = array_values( $actual );
1482  }
1483 
1484  call_user_func_array(
1485  [ $this, 'assertEquals' ],
1486  array_merge( [ $expected, $actual ], array_slice( func_get_args(), 4 ) )
1487  );
1488  }
1489 
1502  protected function assertHTMLEquals( $expected, $actual, $msg = '' ) {
1503  $expected = str_replace( '>', ">\n", $expected );
1504  $actual = str_replace( '>', ">\n", $actual );
1505 
1506  $this->assertEquals( $expected, $actual, $msg );
1507  }
1508 
1516  protected function objectAssociativeSort( array &$array ) {
1517  uasort(
1518  $array,
1519  function ( $a, $b ) {
1520  return serialize( $a ) > serialize( $b ) ? 1 : -1;
1521  }
1522  );
1523  }
1524 
1534  protected static function stripStringKeys( &$r ) {
1535  if ( !is_array( $r ) ) {
1536  return;
1537  }
1538 
1539  foreach ( $r as $k => $v ) {
1540  if ( is_string( $k ) ) {
1541  unset( $r[$k] );
1542  }
1543  }
1544  }
1545 
1559  protected function assertTypeOrValue( $type, $actual, $value = false, $message = '' ) {
1560  if ( $actual === $value ) {
1561  $this->assertTrue( true, $message );
1562  } else {
1563  $this->assertType( $type, $actual, $message );
1564  }
1565  }
1566 
1578  protected function assertType( $type, $actual, $message = '' ) {
1579  if ( class_exists( $type ) || interface_exists( $type ) ) {
1580  $this->assertInstanceOf( $type, $actual, $message );
1581  } else {
1582  $this->assertInternalType( $type, $actual, $message );
1583  }
1584  }
1585 
1595  protected function isWikitextNS( $ns ) {
1596  global $wgNamespaceContentModels;
1597 
1598  if ( isset( $wgNamespaceContentModels[$ns] ) ) {
1599  return $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT;
1600  }
1601 
1602  return true;
1603  }
1604 
1612  protected function getDefaultWikitextNS() {
1613  global $wgNamespaceContentModels;
1614 
1615  static $wikitextNS = null; // this is not going to change
1616  if ( $wikitextNS !== null ) {
1617  return $wikitextNS;
1618  }
1619 
1620  // quickly short out on most common case:
1621  if ( !isset( $wgNamespaceContentModels[NS_MAIN] ) ) {
1622  return NS_MAIN;
1623  }
1624 
1625  // NOTE: prefer content namespaces
1626  $namespaces = array_unique( array_merge(
1628  [ NS_MAIN, NS_HELP, NS_PROJECT ], // prefer these
1630  ) );
1631 
1632  $namespaces = array_diff( $namespaces, [
1633  NS_FILE, NS_CATEGORY, NS_MEDIAWIKI, NS_USER // don't mess with magic namespaces
1634  ] );
1635 
1636  $talk = array_filter( $namespaces, function ( $ns ) {
1637  return MWNamespace::isTalk( $ns );
1638  } );
1639 
1640  // prefer non-talk pages
1641  $namespaces = array_diff( $namespaces, $talk );
1642  $namespaces = array_merge( $namespaces, $talk );
1643 
1644  // check default content model of each namespace
1645  foreach ( $namespaces as $ns ) {
1646  if ( !isset( $wgNamespaceContentModels[$ns] ) ||
1647  $wgNamespaceContentModels[$ns] === CONTENT_MODEL_WIKITEXT
1648  ) {
1649 
1650  $wikitextNS = $ns;
1651 
1652  return $wikitextNS;
1653  }
1654  }
1655 
1656  // give up
1657  // @todo Inside a test, we could skip the test as incomplete.
1658  // But frequently, this is used in fixture setup.
1659  throw new MWException( "No namespace defaults to wikitext!" );
1660  }
1661 
1668  protected function markTestSkippedIfNoDiff3() {
1669  global $wgDiff3;
1670 
1671  # This check may also protect against code injection in
1672  # case of broken installations.
1673  MediaWiki\suppressWarnings();
1674  $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 );
1675  MediaWiki\restoreWarnings();
1676 
1677  if ( !$haveDiff3 ) {
1678  $this->markTestSkipped( "Skip test, since diff3 is not configured" );
1679  }
1680  }
1681 
1690  protected function checkPHPExtension( $extName ) {
1691  $loaded = extension_loaded( $extName );
1692  if ( !$loaded ) {
1693  $this->markTestSkipped( "PHP extension '$extName' is not loaded, skipping." );
1694  }
1695 
1696  return $loaded;
1697  }
1698 
1713  protected function assertValidHtmlSnippet( $html ) {
1714  $html = '<!DOCTYPE html><html><head><title>test</title></head><body>' . $html . '</body></html>';
1715  $this->assertValidHtmlDocument( $html );
1716  }
1717 
1729  protected function assertValidHtmlDocument( $html ) {
1730  // Note: we only validate if the tidy PHP extension is available.
1731  // In case wgTidyInternal is false, MWTidy would fall back to the command line version
1732  // of tidy. In that case however, we can not reliably detect whether a failing validation
1733  // is due to malformed HTML, or caused by tidy not being installed as a command line tool.
1734  // That would cause all HTML assertions to fail on a system that has no tidy installed.
1735  if ( !$GLOBALS['wgTidyInternal'] || !MWTidy::isEnabled() ) {
1736  $this->markTestSkipped( 'Tidy extension not installed' );
1737  }
1738 
1739  $errorBuffer = '';
1740  MWTidy::checkErrors( $html, $errorBuffer );
1741  $allErrors = preg_split( '/[\r\n]+/', $errorBuffer );
1742 
1743  // Filter Tidy warnings which aren't useful for us.
1744  // Tidy eg. often cries about parameters missing which have actually
1745  // been deprecated since HTML4, thus we should not care about them.
1746  $errors = preg_grep(
1747  '/^(.*Warning: (trimming empty|.* lacks ".*?" attribute).*|\s*)$/m',
1748  $allErrors, PREG_GREP_INVERT
1749  );
1750 
1751  $this->assertEmpty( $errors, implode( "\n", $errors ) );
1752  }
1753 
1761  private static function tagMatch( $matcher, $actual, $isHtml = true ) {
1762  $dom = PHPUnit_Util_XML::load( $actual, $isHtml );
1763  $tags = PHPUnit_Util_XML::findNodes( $dom, $matcher, $isHtml );
1764  return count( $tags ) > 0 && $tags[0] instanceof DOMNode;
1765  }
1766 
1778  public static function assertTag( $matcher, $actual, $message = '', $isHtml = true ) {
1779  // trigger_error(__METHOD__ . ' is deprecated', E_USER_DEPRECATED);
1780 
1781  self::assertTrue( self::tagMatch( $matcher, $actual, $isHtml ), $message );
1782  }
1783 
1793  public static function assertNotTag( $matcher, $actual, $message = '', $isHtml = true ) {
1794  // trigger_error(__METHOD__ . ' is deprecated', E_USER_DEPRECATED);
1795 
1796  self::assertFalse( self::tagMatch( $matcher, $actual, $isHtml ), $message );
1797  }
1798 
1803  public static function wfResetOutputBuffersBarrier( $buffer ) {
1804  return $buffer;
1805  }
1806 
1814  protected function setTemporaryHook( $hookName, $handler ) {
1815  $this->mergeMwGlobalArrayValue( 'wgHooks', [ $hookName => [ $handler ] ] );
1816  }
1817 
1818 }
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:115
mergeMwGlobalArrayValue($name, $values)
Merges the given values into a MW global array variable.
const DB_PREFIX
Table name prefixes.
checkPHPExtension($extName)
Check if $extName is a loaded PHP extension, will skip the test whenever it is not loaded...
static $additionalOptions
Definition: phpunit.php:18
static getMainWANInstance()
Get the main WAN cache object.
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses & $html
Definition: hooks.txt:1936
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()
wfGetDB($db, $groups=[], $wiki=false)
Get a Database object.
static clearPendingUpdates()
Clear all pending updates without performing them.
static wfResetOutputBuffersBarrier($buffer)
Used as a marker to prevent wfResetOutputBuffers from breaking PHPUnit.
static installTestServices(ConfigFactory $oldConfigFactory, MediaWikiServices $newServices)
the array() calling protocol came about after MediaWiki 1.4rc1.
static assertNotTag($matcher, $actual, $message= '', $isHtml=true)
const CONTENT_MODEL_WIKITEXT
Definition: Defines.php:239
select($table, $vars, $conds= '', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
Definition: Database.php:1250
array $wgDefaultExternalStore
The place to put new revisions, false to put them in the local text table.
static setLBFactoryTriggers(LBFactory $LBFactory)
const NS_MAIN
Definition: Defines.php:56
$called
$called tracks whether the setUp and tearDown method has been called.
const CACHE_ACCEL
Definition: Defines.php:97
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:189
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
wfMkdirParents($dir, $mode=null, $caller=null)
Make directory, and make all parent directories if they don't exist.
if(!$wgDBerrorLogTZ) $wgRequest
Definition: Setup.php:664
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException'returning false will NOT prevent logging $e
Definition: hooks.txt:2102
static isEnabled()
Definition: MWTidy.php:79
assertValidHtmlDocument($html)
Asserts that the given string is valid HTML document.
if(!isset($args[0])) $lang
static isTalk($index)
Is the given namespace a talk namespace?
Definition: MWNamespace.php:97
$comment
MediaWiki has optional support for a high distributed memory object caching system For general information on but for a larger site with heavy load
Definition: memcached.txt:1
assertArrayEquals(array $expected, array $actual, $ordered=false, $named=false)
Assert that two arrays are equal.
run(PHPUnit_Framework_TestResult $result=null)
static checkErrors($text, &$errorStr=null)
Check HTML for errors, used if $wgValidateAllHtml = true.
Definition: MWTidy.php:63
$value
static setupDatabaseWithTestPrefix(Database $db, $prefix)
Setups a database with the given prefix.
static changePrefix($prefix)
Change the table prefix on all open DB connections/.
$files
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
assertType($type, $actual, $message= '')
Asserts the type of the provided value.
lastError()
Get a description of the last error.
__construct($name=null, array $data=[], $dataName= '')
static resetIdByNameCache()
Reset the cache used in idFromName().
Definition: User.php:768
static newFromText($text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:262
when a variable name is used in a it is silently declared as a new local masking the global
Definition: design.txt:93
static destroySingleton()
Destroy the singleton instance.
tableName($name, $format= 'quoted')
Format a table name ready for use in constructing an SQL query.
Definition: Database.php:1696
LoggerFactory service provider that creates loggers implemented by Monolog.
Definition: MonologSpi.php:116
getNewTempFile()
Obtains a new temporary file name.
getType()
Get the type of the DBMS, as it appears in $wgDBtype.
const DB_MASTER
Definition: defines.php:23
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 RecentChangesLinked and Watchlist e g Watchlist & $tables
Definition: hooks.txt:1007
setService($name, $object)
Sets a service, maintaining a stashed version of the previous service to be restored in tearDown...
const CACHE_MEMCACHED
Definition: Defines.php:96
static tagMatch($matcher, $actual, $isHtml=true)
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message.Please note the header message cannot receive/use parameters. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item.Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page.Return false to stop further processing of the tag $reader:XMLReader object &$pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision.Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag.Return false to stop further processing of the tag $reader:XMLReader object 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload.Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports.&$fullInterwikiPrefix:Interwiki prefix, may contain colons.&$pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable.Can be used to lazy-load the import sources list.&$importSources:The value of $wgImportSources.Modify as necessary.See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page.$context:IContextSource object &$pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect.&$title:Title object for the current page &$request:WebRequest &$ignoreRedirect:boolean to skip redirect check &$target:Title/string of redirect target &$article:Article object 'InternalParseBeforeLinks':during Parser's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings.&$parser:Parser object &$text:string containing partially parsed text &$stripState:Parser's internal StripState object 'InternalParseBeforeSanitize':during Parser's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings.Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments.&$parser:Parser object &$text:string containing partially parsed text &$stripState:Parser's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not.Return true without providing an interwiki to continue interwiki search.$prefix:interwiki prefix we are looking for.&$iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user's email has been invalidated successfully.$user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification.Callee may modify $url and $query, URL will be constructed as $url.$query &$url:URL to index.php &$query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) &$article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() &$ip:IP being check &$result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from &$allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn't match your organization.$addr:The e-mail address entered by the user &$result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user &$result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we're looking for a messages file for &$file:The messages file path, you can override this to change the location. 'LanguageGetMagic':DEPRECATED!Use $magicWords in a file listed in $wgExtensionMessagesFiles instead.Use this to define synonyms of magic words depending of the language &$magicExtensions:associative array of magic words synonyms $lang:language code(string) 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces.Do not use this hook to add namespaces.Use CanonicalNamespaces for that.&$namespaces:Array of namespaces indexed by their numbers 'LanguageGetSpecialPageAliases':DEPRECATED!Use $specialPageAliases in a file listed in $wgExtensionMessagesFiles instead.Use to define aliases of special pages names depending of the language &$specialPageAliases:associative array of magic words synonyms $lang:language code(string) 'LanguageGetTranslatedLanguageNames':Provide translated language names.&$names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page's language links.This is called in various places to allow extensions to define the effective language links for a page.$title:The page's Title.&$links:Associative array mapping language codes to prefixed links of the form"language:title".&$linkFlags:Associative array mapping prefixed links to arrays of flags.Currently unused, but planned to provide support for marking individual language links in the UI, e.g.for featured articles. 'LanguageSelector':Hook to change the language selector available on a page.$out:The output page.$cssClassName:CSS class name of the language selector. 'LinkBegin':DEPRECATED!Use HtmlPageLinkRendererBegin instead.Used when generating internal and interwiki links in Linker::link(), before processing starts.Return false to skip default processing and return $ret.See documentation for Linker::link() for details on the expected meanings of parameters.$skin:the Skin object $target:the Title that the link is pointing to &$html:the contents that the< a > tag should have(raw HTML) $result
Definition: hooks.txt:1934
if($line===false) $args
Definition: cdb.php:64
restoreLoggers()
Restores loggers replaced by setLogger().
getDefaultWikitextNS()
Returns the ID of a namespace that defaults to Wikitext.
static clear()
Clear the registry.
resetServiceForTesting($name, $destroy=true)
Resets the given service for testing purposes.
setupAllTestDBs()
Set up all test DBs.
static getTestSysop()
Convenience method for getting an immutable admin test user.
const NS_PROJECT
Definition: Defines.php:60
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.
setCliArg($offset, $value)
static getMain()
Static methods.
static prepareServices(Config $bootstrapConfig)
Prepare service configuration for unit testing.
static singleton()
Get an instance of this class.
Definition: LinkCache.php:64
unserialize($serialized)
Definition: ApiMessage.php:102
$GLOBALS['IP']
static assertTag($matcher, $actual, $message= '', $isHtml=true)
Note: we are overriding this method to remove the deprecated error.
static isNotUnittest($table)
static getStoreObject($proto, array $params=[])
Get an external store object of the given type, with the given parameters.
static teardownTestDB()
Restores MediaWiki to using the table set (table prefix) it was using before setupTestDB() was called...
static setupExternalStoreTestDBs($testPrefix)
Clones the External Store database(s) for testing.
$res
Definition: database.txt:21
makeConfig($name)
Create a given Config using the registered callback for $name.
objectAssociativeSort(array &$array)
Does an associative sort that works for objects.
static getExternalStoreDatabaseConnections()
Gets master database connections for all of the ExternalStoreDB stores configured in $wgDefaultExtern...
MediaWiki Logger MonologSpi
Definition: logger.txt:58
assertSelect($table, $fields, $condition, array $expectedRows)
Asserts that the given database query yields the rows given by $expectedRows.
const NS_CATEGORY
Definition: Defines.php:70
static resetMain()
Resets singleton returned by getMain().
wfDeprecated($function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
static getContentNamespaces()
Get a list of all namespace indices which are considered to contain content.
static getMutableTestUser($testName, $groups=[])
Get a TestUser object that the caller may modify.
LoggerFactory service provider that creates LegacyLogger instances.
Definition: LegacySpi.php:38
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:953
hideDeprecated($function)
Don't throw a warning if $function is deprecated and called later.
static run($event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:131
const NS_FILE
Definition: Defines.php:62
static makeContent($text, Title $title=null, $modelId=null, $format=null)
Convenience function for creating a Content object from a given textual representation.
Content object for wiki text pages.
overrideMwServices(Config $configOverrides=null, array $services=[])
Stashes the global instance of MediaWikiServices, and installs a new one, allowing test cases to over...
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title after the basic globals have been set but before ordinary actions take place or wrap services the preferred way to define a new service is the $wgServiceWiringFiles array $services
Definition: hooks.txt:2159
Provides a fallback sequence for Config objects.
Definition: MultiConfig.php:28
namespace and then decline to actually register it & $namespaces
Definition: hooks.txt:953
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.
const NS_MEDIAWIKI
Definition: Defines.php:64
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account $user
Definition: hooks.txt:242
array $mwGlobals
Holds original values of MediaWiki configuration settings to be restored in tearDown().
static getTestUser($groups=[])
Convenience method for getting an immutable test user.
static TestUser[] $users
redefineService($name, callable $instantiator)
Replace an already defined service.
assertTypeOrValue($type, $actual, $value=false, $message= '')
Asserts that the provided variable is of the specified internal type or equals the $value argument...
static singleton($wiki=false)
doLightweightServiceReset()
Resets some well known services that typically have state that may interfere with unit tests...
int $phpErrorLevel
Original value of PHP's error_reporting setting.
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
or there are no hooks to run
Definition: hooks.txt:223
MediaWiki Logger LegacySpi
Definition: logger.txt:53
const NS_HELP
Definition: Defines.php:68
$lbFactory
insertPage($pageName, $text= 'Sample page for unit test.', $namespace=null)
Insert a new page.
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:146
static resetGlobalServices(Config $bootstrapConfig=null)
Reset global services, and install testing environment.
markTestSkippedIfNoDiff3()
Check, if $wgDiff3 is set and ready to merge Will mark the calling test as skipped, if not ready.
listViews($prefix=null, $fname=__METHOD__)
Lists all the VIEWs in the database.
Definition: Database.php:2904
static MediaWikiServices null $serviceLocator
The service locator created by prepareServices().
$wgDBprefix
Table name prefix.
isWikitextNS($ns)
Returns true if the given namespace defaults to Wikitext according to $wgNamespaceContentModels.
wfRecursiveRemoveDir($dir)
Remove a directory and all its content.
static getImmutableTestUser($groups=[])
Get a TestUser object that the caller may not modify.
static unprefixTable(&$tableName, $ind, $prefix)
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set $status
Definition: hooks.txt:1046
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 getMutableTestUser($groups=[])
Convenience method for getting a mutable test user.
static listTables(Database $db)
getNewTempDirectory()
obtains a new temporary directory
static makeTestConfigFactoryInstantiator(ConfigFactory $oldFactory, array $configurations)
array $tmpFiles
Holds the paths of temporary files/directories created through getNewTempFile, and getNewTempDirector...
getBootstrapConfig()
Returns the Config object containing the bootstrap configuration.
static clear()
Clear all the cached instances.
serialize()
Definition: ApiMessage.php:94
static setupTestDB(Database $db, $prefix)
Creates an empty skeleton of the wiki database by cloning its structure to equivalent tables using th...
static newFromObject($object)
Return the same object, without access restrictions.
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check set to true or false to override the $wgMaxImageArea check result gives extension the possibility to transform it themselves $handler
Definition: hooks.txt:802
const CACHE_NONE
Definition: Defines.php:94
$wgSQLMode
SQL Mode - default is turning off all modes, including strict, if set.
static factory($code)
Get a cached or new language object for a given language code.
Definition: Language.php:181
setMwGlobals($pairs, $value=null)
const DB_SLAVE
Definition: Defines.php:28
A Config instance which stores all settings as a member variable.
Definition: HashConfig.php:28
static getValidNamespaces()
Returns an array of the namespaces (by integer id) that exist on the wiki.
setLogger($channel, LoggerInterface $logger)
Sets the logger for a specified channel, for the duration of the test.
MediaWikiServices is the service locator for the application scope of MediaWiki.
setTemporaryHook($hookName, $handler)
Create a temporary hook handler which will be reset by tearDown.
assertEmpty2($value, $msg)
Used as a compatibility method for phpunit < 3.7.32.
assertHTMLEquals($expected, $actual, $msg= '')
Put each HTML element on its own line and then equals() the results.
do that in ParserLimitReportFormat instead use this to modify the parameters of the image and a DIV can begin in one section and end in another Make sure your code can handle that case gracefully See the EditSectionClearerLink extension for an example zero but section is usually empty its values are the globals values before the output is cached one of or reset my talk my contributions etc etc otherwise the built in rate limiting checks are if enabled allows for interception of redirect as a string mapping parameter names to values & $type
Definition: hooks.txt:2491
static stripStringKeys(&$r)
Utility function for eliminating all string keys from an array.
tablePrefix($prefix=null)
Get/set the table prefix.
Definition: Database.php:448
delete($table, $conds, $fname=__METHOD__)
DELETE query wrapper.
Definition: Database.php:2254
stashMwGlobals($globalKeys)
Stashes the global, will be restored in tearDown()
wfGetCaller($level=2)
Get the name of the function which called this function wfGetCaller( 1 ) is the function with the wfG...
listTables($prefix=null, $fname=__METHOD__)
List all tables on the database.
Definition: Database.php:2900
resetDB($db, $tablesUsed)
Empty all tables so they can be repopulated for tests.
const CACHE_DB
Definition: Defines.php:95
do that in ParserLimitReportFormat instead use this to modify the parameters of the image and a DIV can begin in one section and end in another Make sure your code can handle that case gracefully See the EditSectionClearerLink extension for an example zero but section is usually empty its values are the globals values before the output is cached $page
Definition: hooks.txt:2491
query($sql, $fname=__METHOD__, $tempIgnore=false)
Run an SQL query and return the result.
Definition: Database.php:829
testMediaWikiTestCaseParentSetupCalled()
Make sure MediaWikiTestCase extending classes have called their parent setUp method.
static destroySingletons()
Destroy the singleton instances.
static canShallowCopy($value)
Check if we can back up a value by performing a shallow copy.
assertValidHtmlSnippet($html)
Asserts that the given string is a valid HTML snippet.
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:300