X-Git-Url: http://git.cyclocoop.org/?a=blobdiff_plain;f=tests%2Fphpunit%2FMediaWikiTestCase.php;h=8dfe628a3ff40bd595fb14970a7c31c7b4c9e235;hb=fa40f8f1451a1b6771718b8fde691fca42c4f9c2;hp=dc6990260a69e1687d616aa246afae9ce00b3848;hpb=1024249502ed39f5a5d1871b3112de71186e4432;p=lhc%2Fweb%2Fwiklou.git diff --git a/tests/phpunit/MediaWikiTestCase.php b/tests/phpunit/MediaWikiTestCase.php index dc6990260a..8dfe628a3f 100644 --- a/tests/phpunit/MediaWikiTestCase.php +++ b/tests/phpunit/MediaWikiTestCase.php @@ -2,12 +2,23 @@ use MediaWiki\Logger\LegacySpi; use MediaWiki\Logger\LoggerFactory; use MediaWiki\Logger\MonologSpi; +use MediaWiki\MediaWikiServices; use Psr\Log\LoggerInterface; /** * @since 1.18 */ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { + + /** + * The service locator created by prepareServices(). This service locator will + * be restored after each test. Tests that pollute the global service locator + * instance should use overrideMwServices() to isolate the test. + * + * @var MediaWikiServices|null + */ + private static $serviceLocator = null; + /** * $called tracks whether the setUp and tearDown method has been called. * class extending MediaWikiTestCase usually override setUp and tearDown @@ -108,22 +119,245 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { } } - public function run( PHPUnit_Framework_TestResult $result = null ) { + public static function setUpBeforeClass() { + parent::setUpBeforeClass(); + + // NOTE: Usually, PHPUnitMaintClass::finalSetup already called this, + // but let's make doubly sure. + self::prepareServices( new GlobalVarConfig() ); + } + + /** + * Convenience method for getting an immutable test user + * + * @since 1.28 + * + * @param string[] $groups Groups the test user should be in. + * @return TestUser + */ + public static function getTestUser( $groups = [] ) { + return TestUserRegistry::getImmutableTestUser( $groups ); + } + + /** + * Convenience method for getting a mutable test user + * + * @since 1.28 + * + * @param string[] $groups Groups the test user should be added in. + * @return TestUser + */ + public static function getMutableTestUser( $groups = [] ) { + return TestUserRegistry::getMutableTestUser( __CLASS__, $groups ); + } + + /** + * Convenience method for getting an immutable admin test user + * + * @since 1.28 + * + * @param string[] $groups Groups the test user should be added to. + * @return TestUser + */ + public static function getTestSysop() { + return self::getTestUser( [ 'sysop', 'bureaucrat' ] ); + } + + /** + * Prepare service configuration for unit testing. + * + * This calls MediaWikiServices::resetGlobalInstance() to allow some critical services + * to be overridden for testing. + * + * prepareServices() only needs to be called once, but should be called as early as possible, + * before any class has a chance to grab a reference to any of the global services + * instances that get discarded by prepareServices(). Only the first call has any effect, + * later calls are ignored. + * + * @note This is called by PHPUnitMaintClass::finalSetup. + * + * @see MediaWikiServices::resetGlobalInstance() + * + * @param Config $bootstrapConfig The bootstrap config to use with the new + * MediaWikiServices. Only used for the first call to this method. + */ + public static function prepareServices( Config $bootstrapConfig ) { + static $servicesPrepared = false; + + if ( $servicesPrepared ) { + return; + } else { + $servicesPrepared = true; + } + + self::resetGlobalServices( $bootstrapConfig ); + } + + /** + * Reset global services, and install testing environment. + * This is the testing equivalent of MediaWikiServices::resetGlobalInstance(). + * This should only be used to set up the testing environment, not when + * running unit tests. Use overrideMwServices() for that. + * + * @see MediaWikiServices::resetGlobalInstance() + * @see prepareServices() + * @see overrideMwServices() + * + * @param Config|null $bootstrapConfig The bootstrap config to use with the new + * MediaWikiServices. + */ + protected static function resetGlobalServices( Config $bootstrapConfig = null ) { + $oldServices = MediaWikiServices::getInstance(); + $oldConfigFactory = $oldServices->getConfigFactory(); + + $testConfig = self::makeTestConfig( $bootstrapConfig ); + + MediaWikiServices::resetGlobalInstance( $testConfig ); + + self::$serviceLocator = MediaWikiServices::getInstance(); + self::installTestServices( + $oldConfigFactory, + self::$serviceLocator + ); + } + + /** + * Create a config suitable for testing, based on a base config, default overrides, + * and custom overrides. + * + * @param Config|null $baseConfig + * @param Config|null $customOverrides + * + * @return Config + */ + private static function makeTestConfig( + Config $baseConfig = null, + Config $customOverrides = null + ) { + $defaultOverrides = new HashConfig(); + + if ( !$baseConfig ) { + $baseConfig = MediaWikiServices::getInstance()->getBootstrapConfig(); + } + /* Some functions require some kind of caching, and will end up using the db, * which we can't allow, as that would open a new connection for mysql. * Replace with a HashBag. They would not be going to persist anyway. */ - ObjectCache::$instances[CACHE_DB] = new HashBagOStuff; + $hashCache = [ 'class' => 'HashBagOStuff' ]; + $objectCaches = [ + CACHE_DB => $hashCache, + CACHE_ACCEL => $hashCache, + CACHE_MEMCACHED => $hashCache, + 'apc' => $hashCache, + 'xcache' => $hashCache, + 'wincache' => $hashCache, + ] + $baseConfig->get( 'ObjectCaches' ); + + $defaultOverrides->set( 'ObjectCaches', $objectCaches ); + $defaultOverrides->set( 'MainCacheType', CACHE_NONE ); + + // Use a fast hash algorithm to hash passwords. + $defaultOverrides->set( 'PasswordDefault', 'A' ); + + $testConfig = $customOverrides + ? new MultiConfig( [ $customOverrides, $defaultOverrides, $baseConfig ] ) + : new MultiConfig( [ $defaultOverrides, $baseConfig ] ); + + return $testConfig; + } + + /** + * @param ConfigFactory $oldConfigFactory + * @param MediaWikiServices $newServices + * + * @throws MWException + */ + private static function installTestServices( + ConfigFactory $oldConfigFactory, + MediaWikiServices $newServices + ) { + // Use bootstrap config for all configuration. + // This allows config overrides via global variables to take effect. + $bootstrapConfig = $newServices->getBootstrapConfig(); + $newServices->resetServiceForTesting( 'ConfigFactory' ); + $newServices->redefineService( + 'ConfigFactory', + self::makeTestConfigFactoryInstantiator( + $oldConfigFactory, + [ 'main' => $bootstrapConfig ] + ) + ); + } + + /** + * @param ConfigFactory $oldFactory + * @param Config[] $configurations + * + * @return Closure + */ + private static function makeTestConfigFactoryInstantiator( + ConfigFactory $oldFactory, + array $configurations + ) { + return function( MediaWikiServices $services ) use ( $oldFactory, $configurations ) { + $factory = new ConfigFactory(); + + // clone configurations from $oldFactory that are not overwritten by $configurations + $namesToClone = array_diff( + $oldFactory->getConfigNames(), + array_keys( $configurations ) + ); + + foreach ( $namesToClone as $name ) { + $factory->register( $name, $oldFactory->makeConfig( $name ) ); + } + + foreach ( $configurations as $name => $config ) { + $factory->register( $name, $config ); + } + + return $factory; + }; + } + + /** + * Resets some well known services that typically have state that may interfere with unit tests. + * This is a lightweight alternative to resetGlobalServices(). + * + * @note There is no guarantee that no references remain to stale service instances destroyed + * by a call to doLightweightServiceReset(). + * + * @throws MWException if called outside of PHPUnit tests. + * + * @see resetGlobalServices() + */ + private function doLightweightServiceReset() { + global $wgRequest; + + JobQueueGroup::destroySingletons(); + ObjectCache::clear(); + FileBackendGroup::destroySingleton(); + + // TODO: move global state into MediaWikiServices + RequestContext::resetMain(); + MediaHandler::resetCache(); + if ( session_id() !== '' ) { + session_write_close(); + session_id( '' ); + } + + $wgRequest = new FauxRequest(); + MediaWiki\Session\SessionManager::resetCache(); + } - // Sandbox APC by replacing with in-process hash instead. - // Ensures values are removed between tests. - ObjectCache::$instances['apc'] = - ObjectCache::$instances['xcache'] = - ObjectCache::$instances['wincache'] = new HashBagOStuff; + public function run( PHPUnit_Framework_TestResult $result = null ) { + // Reset all caches between tests. + $this->doLightweightServiceReset(); $needsResetDB = false; - if ( $this->needsDB() ) { + if ( !self::$dbSetup || $this->needsDB() ) { // set up a DB connection for this test to use self::$useTemporaryTables = !$this->getCliArg( 'use-normal-tables' ); @@ -161,7 +395,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { } /** - * @return boolean + * @return bool */ private function oncePerClass() { // Remember current test class in the database connection, @@ -247,6 +481,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { } DeferredUpdates::clearPendingUpdates(); + ObjectCache::getMainWANInstance()->clearProcessCache(); ob_start( 'MediaWikiTestCase::wfResetOutputBuffersBarrier' ); } @@ -288,6 +523,12 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { } $this->mwGlobals = []; $this->restoreLoggers(); + + if ( self::$serviceLocator && MediaWikiServices::getInstance() !== self::$serviceLocator ) { + MediaWikiServices::forceGlobalInstance( self::$serviceLocator ); + } + + // TODO: move global state into MediaWikiServices RequestContext::resetMain(); MediaHandler::resetCache(); if ( session_id() !== '' ) { @@ -296,6 +537,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { } $wgRequest = new FauxRequest(); MediaWiki\Session\SessionManager::resetCache(); + MediaWiki\Auth\AuthManager::resetCache(); $phpErrorLevel = intval( ini_get( 'error_reporting' ) ); @@ -323,6 +565,30 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { ); } + /** + * Sets a service, maintaining a stashed version of the previous service to be + * restored in tearDown + * + * @since 1.27 + * + * @param string $name + * @param object $object + */ + protected function setService( $name, $object ) { + // If we did not yet override the service locator, so so now. + if ( MediaWikiServices::getInstance() === self::$serviceLocator ) { + $this->overrideMwServices(); + } + + MediaWikiServices::getInstance()->disableService( $name ); + MediaWikiServices::getInstance()->redefineService( + $name, + function () use ( $object ) { + return $object; + } + ); + } + /** * Sets a global, maintaining a stashed version of the previous global to be * restored in tearDown @@ -353,6 +619,9 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { * @param mixed $value Value to set the global to (ignored * if an array is given as first argument). * + * @note To allow changes to global variables to take effect on global service instances, + * call overrideMwServices(). + * * @since 1.21 */ protected function setMwGlobals( $pairs, $value = null ) { @@ -367,6 +636,29 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { } } + /** + * Check if we can back up a value by performing a shallow copy. + * Values which fail this test are copied recursively. + * + * @param mixed $value + * @return bool True if a shallow copy will do; false if a deep copy + * is required. + */ + private static function canShallowCopy( $value ) { + if ( is_scalar( $value ) || $value === null ) { + return true; + } + if ( is_array( $value ) ) { + foreach ( $value as $subValue ) { + if ( !is_scalar( $subValue ) && $subValue !== null ) { + return false; + } + } + return true; + } + return false; + } + /** * Stashes the global, will be restored in tearDown() * @@ -380,6 +672,10 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { * @param array|string $globalKeys Key to the global variable, or an array of keys. * * @throws Exception When trying to stash an unset global + * + * @note To allow changes to global variables to take effect on global service instances, + * call overrideMwServices(). + * * @since 1.23 */ protected function stashMwGlobals( $globalKeys ) { @@ -398,13 +694,22 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { // NOTE: we serialize then unserialize the value in case it is an object // this stops any objects being passed by reference. We could use clone // and if is_object but this does account for objects within objects! - try { - $this->mwGlobals[$globalKey] = unserialize( serialize( $GLOBALS[$globalKey] ) ); - } - // NOTE; some things such as Closures are not serializable - // in this case just set the value! - catch ( Exception $e ) { + if ( self::canShallowCopy( $GLOBALS[$globalKey] ) ) { $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey]; + } elseif ( + // Many MediaWiki types are safe to clone. These are the + // ones that are most commonly stashed. + $GLOBALS[$globalKey] instanceof Language || + $GLOBALS[$globalKey] instanceof User || + $GLOBALS[$globalKey] instanceof FauxRequest + ) { + $this->mwGlobals[$globalKey] = clone $GLOBALS[$globalKey]; + } else { + try { + $this->mwGlobals[$globalKey] = unserialize( serialize( $GLOBALS[$globalKey] ) ); + } catch ( Exception $e ) { + $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey]; + } } } } @@ -420,6 +725,9 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { * * @throws MWException If the designated global is not an array. * + * @note To allow changes to global variables to take effect on global service instances, + * call overrideMwServices(). + * * @since 1.21 */ protected function mergeMwGlobalArrayValue( $name, $values ) { @@ -440,6 +748,52 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { $this->setMwGlobals( $name, $merged ); } + /** + * Stashes the global instance of MediaWikiServices, and installs a new one, + * allowing test cases to override settings and services. + * The previous instance of MediaWikiServices will be restored on tearDown. + * + * @since 1.27 + * + * @param Config $configOverrides Configuration overrides for the new MediaWikiServices instance. + * @param callable[] $services An associative array of services to re-define. Keys are service + * names, values are callables. + * + * @return MediaWikiServices + * @throws MWException + */ + protected function overrideMwServices( Config $configOverrides = null, array $services = [] ) { + if ( !$configOverrides ) { + $configOverrides = new HashConfig(); + } + + $oldInstance = MediaWikiServices::getInstance(); + $oldConfigFactory = $oldInstance->getConfigFactory(); + + $testConfig = self::makeTestConfig( null, $configOverrides ); + $newInstance = new MediaWikiServices( $testConfig ); + + // Load the default wiring from the specified files. + // NOTE: this logic mirrors the logic in MediaWikiServices::newInstance. + $wiringFiles = $testConfig->get( 'ServiceWiringFiles' ); + $newInstance->loadWiringFiles( $wiringFiles ); + + // Provide a traditional hook point to allow extensions to configure services. + Hooks::run( 'MediaWikiServices', [ $newInstance ] ); + + foreach ( $services as $name => $callback ) { + $newInstance->redefineService( $name, $callback ); + } + + self::installTestServices( + $oldConfigFactory, + $newInstance + ); + MediaWikiServices::forceGlobalInstance( $newInstance ); + + return $newInstance; + } + /** * @since 1.27 * @param string|Language $lang @@ -474,6 +828,9 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { * @param LoggerInterface $logger */ protected function setLogger( $channel, LoggerInterface $logger ) { + // TODO: Once loggers are managed by MediaWikiServices, use + // overrideMwServices() to set loggers. + $provider = LoggerFactory::getProvider(); $wrappedProvider = TestingAccessWrapper::newFromObject( $provider ); $singletons = $wrappedProvider->singletons; @@ -562,9 +919,13 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { protected function insertPage( $pageName, $text = 'Sample page for unit test.' ) { $title = Title::newFromText( $pageName, 0 ); - $user = User::newFromName( 'UTSysop' ); + $user = static::getTestSysop()->getUser(); $comment = __METHOD__ . ': Sample page for unit test.'; + // Avoid memory leak...? + // LinkCache::singleton()->clear(); + // Maybe. But doing this absolutely breaks $title->isRedirect() when called during unit tests.... + $page = WikiPage::factory( $title ); $page->doEditContent( ContentHandler::makeContent( $text, $title ), $comment, 0, false, $user ); @@ -609,38 +970,33 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { # Insert 0 user to prevent FK violations # Anonymous user - $this->db->insert( 'user', [ - 'user_id' => 0, - 'user_name' => 'Anonymous' ], __METHOD__, [ 'IGNORE' ] ); + if ( !$this->db->selectField( 'user', '1', [ 'user_id' => 0 ] ) ) { + $this->db->insert( 'user', [ + 'user_id' => 0, + 'user_name' => 'Anonymous' ], __METHOD__, [ 'IGNORE' ] ); + } # Insert 0 page to prevent FK violations # Blank page - $this->db->insert( 'page', [ - 'page_id' => 0, - 'page_namespace' => 0, - 'page_title' => ' ', - 'page_restrictions' => null, - 'page_is_redirect' => 0, - 'page_is_new' => 0, - 'page_random' => 0, - 'page_touched' => $this->db->timestamp(), - 'page_latest' => 0, - 'page_len' => 0 ], __METHOD__, [ 'IGNORE' ] ); + if ( !$this->db->selectField( 'page', '1', [ 'page_id' => 0 ] ) ) { + $this->db->insert( 'page', [ + 'page_id' => 0, + 'page_namespace' => 0, + 'page_title' => ' ', + 'page_restrictions' => null, + 'page_is_redirect' => 0, + 'page_is_new' => 0, + 'page_random' => 0, + 'page_touched' => $this->db->timestamp(), + 'page_latest' => 0, + 'page_len' => 0 ], __METHOD__, [ 'IGNORE' ] ); + } } User::resetIdByNameCache(); // Make sysop user - $user = User::newFromName( 'UTSysop' ); - - if ( $user->idForName() == 0 ) { - $user->addToDatabase(); - TestUser::setPasswordForUser( $user, 'UTSysopPassword' ); - } - - // Always set groups, because $this->resetDB() wipes them out - $user->addGroup( 'sysop' ); - $user->addGroup( 'bureaucrat' ); + $user = static::getTestSysop()->getUser(); // Make 1 page with 1 revision $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); @@ -764,6 +1120,9 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { return; } + // TODO: the below should be re-written as soon as LBFactory, LoadBalancer, + // and DatabaseBase no longer use global state. + self::$dbSetup = true; if ( !self::setupDatabaseWithTestPrefix( $db, $prefix ) ) { @@ -850,17 +1209,25 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { */ private function resetDB( $db, $tablesUsed ) { if ( $db ) { + $userTables = [ 'user', 'user_groups', 'user_properties' ]; + $coreDBDataTables = array_merge( $userTables, [ 'page', 'revision' ] ); + + // If any of the user tables were marked as used, we should clear all of them. + if ( array_intersect( $tablesUsed, $userTables ) ) { + $tablesUsed = array_unique( array_merge( $tablesUsed, $userTables ) ); + TestUserRegistry::clear(); + } + $truncate = in_array( $db->getType(), [ 'oracle', 'mysql' ] ); foreach ( $tablesUsed as $tbl ) { - // TODO: reset interwiki and user tables to their original content. - if ( $tbl == 'interwiki' || $tbl == 'user' ) { + // TODO: reset interwiki table to its original content. + if ( $tbl == 'interwiki' ) { continue; } if ( $truncate ) { $db->query( 'TRUNCATE TABLE ' . $db->tableName( $tbl ), __METHOD__ ); } else { - $db->delete( $tbl, '*', __METHOD__ ); } @@ -870,6 +1237,11 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { LinkCache::singleton()->clear(); } } + + if ( array_intersect( $tablesUsed, $coreDBDataTables ) ) { + // Re-add core DB data that was deleted + $this->addCoreDBData(); + } } } @@ -1278,32 +1650,6 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { } } - /** - * Check whether we have the 'gzip' commandline utility, will skip - * the test whenever "gzip -V" fails. - * - * Result is cached at the process level. - * - * @return bool - * - * @since 1.21 - */ - protected function checkHasGzip() { - static $haveGzip; - - if ( $haveGzip === null ) { - $retval = null; - wfShellExec( 'gzip -V', $retval ); - $haveGzip = ( $retval === 0 ); - } - - if ( !$haveGzip ) { - $this->markTestSkipped( "Skip test, requires the gzip utility in PATH" ); - } - - return $haveGzip; - } - /** * Check if $extName is a loaded PHP extension, will skip the * test whenever it is not loaded.