'KkConverter' => __DIR__ . '/languages/classes/LanguageKk.php',
'KuConverter' => __DIR__ . '/languages/classes/LanguageKu.php',
'LBFactory' => __DIR__ . '/includes/db/loadbalancer/LBFactory.php',
- 'LBFactoryFake' => __DIR__ . '/includes/db/loadbalancer/LBFactoryFake.php',
'LBFactoryMulti' => __DIR__ . '/includes/db/loadbalancer/LBFactoryMulti.php',
'LBFactorySimple' => __DIR__ . '/includes/db/loadbalancer/LBFactorySimple.php',
'LBFactorySingle' => __DIR__ . '/includes/db/loadbalancer/LBFactorySingle.php',
'MediaWikiSite' => __DIR__ . '/includes/site/MediaWikiSite.php',
'MediaWikiTitleCodec' => __DIR__ . '/includes/title/MediaWikiTitleCodec.php',
'MediaWikiVersionFetcher' => __DIR__ . '/includes/MediaWikiVersionFetcher.php',
+ 'MediaWiki\\MediaWikiServices' => __DIR__ . '/includes/MediaWikiServices.php',
'MediaWiki\\Languages\\Data\\Names' => __DIR__ . '/languages/data/Names.php',
'MediaWiki\\Languages\\Data\\ZhConversion' => __DIR__ . '/languages/data/ZhConversion.php',
'MediaWiki\\Linker\\LinkTarget' => __DIR__ . '/includes/linker/LinkTarget.php',
'MediaWiki\\Logger\\Monolog\\WikiProcessor' => __DIR__ . '/includes/debug/logger/monolog/WikiProcessor.php',
'MediaWiki\\Logger\\NullSpi' => __DIR__ . '/includes/debug/logger/NullSpi.php',
'MediaWiki\\Logger\\Spi' => __DIR__ . '/includes/debug/logger/Spi.php',
- 'MediaWiki\\MediaWikiServices' => __DIR__ . '/includes/MediaWikiServices.php',
+ 'MediaWiki\\Services\\DestructibleService' => __DIR__ . '/includes/Services/DestructibleService.php',
'MediaWiki\\Services\\ServiceContainer' => __DIR__ . '/includes/Services/ServiceContainer.php',
+ 'MediaWiki\\Services\\NoSuchServiceException' => __DIR__ . '/includes/Services/NoSuchServiceException.php',
+ 'MediaWiki\\Services\\CannotReplaceActiveServiceException' => __DIR__ . '/includes/Services/CannotReplaceActiveServiceException.php',
+ 'MediaWiki\\Services\\ServiceAlreadyDefinedException' => __DIR__ . '/includes/Services/ServiceAlreadyDefinedException.php',
+ 'MediaWiki\\Services\\ServiceDisabledException' => __DIR__ . '/includes/Services/ServiceDisabledException.php',
+ 'MediaWiki\\Services\\ContainerDisabledException' => __DIR__ . '/includes/Services/ContainerDisabledException.php',
'MediaWiki\\Session\\BotPasswordSessionProvider' => __DIR__ . '/includes/session/BotPasswordSessionProvider.php',
'MediaWiki\\Session\\CookieSessionProvider' => __DIR__ . '/includes/session/CookieSessionProvider.php',
'MediaWiki\\Session\\ImmutableSessionProviderWithCookie' => __DIR__ . '/includes/session/ImmutableSessionProviderWithCookie.php',
$request: $wgRequest
$mediaWiki: The $mediawiki object
-'MediaWikiServices': Override services in the default MediaWikiServices instance.
-Extensions may use this to define, replace, or wrap existing services.
-However, the preferred way to define a new service is the $wgServiceWiringFiles array.
+'MediaWikiServices': Called when a global MediaWikiServices instance is
+initialized. Extensions may use this to define, replace, or wrap services.
+However, the preferred way to define a new service is
+the $wgServiceWiringFiles array.
$services: MediaWikiServices
'MessageCache::get': When fetching a message. Can be used to override the key
entry points" such as hook handler functions. See "Migration" below.
+== Service Reset ==
+
+Services get their configuration injected, and changes to global
+configuration variables will not have any effect on services that were already
+instantiated. This would typically be the case for low level services like
+the ConfigFactory or the ObjectCacheManager, which are used during extension
+registration. To address this issue, Setup.php resets the global service
+locator instance by calling MediaWikiServices::resetGlobalInstance() once
+configuration and extension registration is complete.
+
+Note that "unmanaged" legacy services services that manage their own singleton
+must not keep references to services managed by MediaWikiServices, to allow a
+clean reset. After the global MediaWikiServices instance got reset, any such
+references would be stale, and using a stale service will result in an error.
+
+Services should either have all dependencies injected and be themselves managed
+by MediaWikiServices, or they should use the Service Locator pattern, accessing
+service instances via the global MediaWikiServices instance state when needed.
+This ensures that no stale service references remain after a reset.
+
+
== Configuration ==
When the default MediaWikiServices instance is created, a Config object is
* Note 2: use $this->getDB() in maintenance scripts that may be invoked by
* updater to ensure that a proper database is being updated.
*
+ * @todo Replace calls to wfGetDB with calls to LoadBalancer::getConnection()
+ * on an injected instance of LoadBalancer.
+ *
* @return DatabaseBase
*/
function wfGetDB( $db, $groups = [], $wiki = false ) {
/**
* Get a load balancer object.
*
+ * @deprecated since 1.27, use MediaWikiServices::getDBLoadBalancer()
+ * or MediaWikiServices::getDBLoadBalancerFactory() instead.
+ *
* @param string|bool $wiki Wiki ID, or false for the current wiki
* @return LoadBalancer
*/
function wfGetLB( $wiki = false ) {
- return wfGetLBFactory()->getMainLB( $wiki );
+ if ( $wiki === false ) {
+ return \MediaWiki\MediaWikiServices::getInstance()->getDBLoadBalancer();
+ } else {
+ $factory = \MediaWiki\MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ return $factory->getMainLB( $wiki );
+ }
}
/**
* Get the load balancer factory object
*
+ * @deprecated since 1.27, use MediaWikiServices::getDBLoadBalancerFactory() instead.
+ *
* @return LBFactory
*/
function wfGetLBFactory() {
- return LBFactory::singleton();
+ return \MediaWiki\MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
}
/**
<?php
namespace MediaWiki;
+use Config;
use ConfigFactory;
use EventRelayerGroup;
use GlobalVarConfig;
-use Config;
use Hooks;
+use LBFactory;
use Liuggio\StatsdClient\Factory\StatsdDataFactory;
+use LoadBalancer;
use MediaWiki\Services\ServiceContainer;
+use MWException;
+use ResourceLoader;
use SearchEngine;
use SearchEngineConfig;
use SearchEngineFactory;
*/
class MediaWikiServices extends ServiceContainer {
+ /**
+ * @var MediaWikiServices|null
+ */
+ private static $instance = null;
+
/**
* Returns the global default instance of the top level service locator.
*
* @return MediaWikiServices
*/
public static function getInstance() {
- static $instance = null;
-
- if ( $instance === null ) {
+ if ( self::$instance === null ) {
// NOTE: constructing GlobalVarConfig here is not particularly pretty,
// but some information from the global scope has to be injected here,
// even if it's just a file name or database credentials to load
// configuration from.
- $config = new GlobalVarConfig();
- $instance = new self( $config );
+ $bootstrapConfig = new GlobalVarConfig();
+ self::$instance = self::newInstance( $bootstrapConfig );
+ }
- // Load the default wiring from the specified files.
- $wiringFiles = $config->get( 'ServiceWiringFiles' );
- $instance->loadWiringFiles( $wiringFiles );
+ return self::$instance;
+ }
- // Provide a traditional hook point to allow extensions to configure services.
- Hooks::run( 'MediaWikiServices', [ $instance ] );
+ /**
+ * Replaces the global MediaWikiServices instance.
+ *
+ * @note This is for use in PHPUnit tests only!
+ *
+ * @throws MWException if called outside of PHPUnit tests.
+ *
+ * @param MediaWikiServices $services The new MediaWikiServices object.
+ *
+ * @return MediaWikiServices The old MediaWikiServices object, so it can be restored later.
+ */
+ public static function forceGlobalInstance( MediaWikiServices $services ) {
+ if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
+ throw new MWException( __METHOD__ . ' must not be used outside unit tests.' );
}
+ $old = self::getInstance();
+ self::$instance = $services;
+
+ return $old;
+ }
+
+ /**
+ * Creates a new instance of MediaWikiServices and sets it as the global default
+ * instance. getInstance() will return a different MediaWikiServices object
+ * after every call to resetGlobalServiceLocator().
+ *
+ * @warning This should not be used during normal operation. It is intended for use
+ * when the configuration has changed significantly since bootstrap time, e.g.
+ * during the installation process or during testing.
+ *
+ * @warning Calling resetGlobalServiceLocator() may leave the application in an inconsistent
+ * state. Calling this is only safe under the ASSUMPTION that NO REFERENCE to
+ * any of the services managed by MediaWikiServices exist. If any service objects
+ * managed by the old MediaWikiServices instance remain in use, they may INTERFERE
+ * with the operation of the services managed by the new MediaWikiServices.
+ * Operating with a mix of services created by the old and the new
+ * MediaWikiServices instance may lead to INCONSISTENCIES and even DATA LOSS!
+ * Any class implementing LAZY LOADING is especially prone to this problem,
+ * since instances would typically retain a reference to a storage layer service.
+ *
+ * @see forceGlobalInstance()
+ * @see resetGlobalInstance()
+ * @see resetBetweenTest()
+ *
+ * @param Config|null $bootstrapConfig The Config object to be registered as the
+ * 'BootstrapConfig' service. This has to contain at least the information
+ * needed to set up the 'ConfigFactory' service. If not given, the bootstrap
+ * config of the old instance of MediaWikiServices will be re-used. If there
+ * was no previous instance, a new GlobalVarConfig object will be used to
+ * bootstrap the services.
+ *
+ * @throws MWException If called after MW_SERVICE_BOOTSTRAP_COMPLETE has been defined in
+ * Setup.php (unless MW_PHPUNIT_TEST or MEDIAWIKI_INSTALL or RUN_MAINTENANCE_IF_MAIN
+ * is defined).
+ */
+ public static function resetGlobalInstance( Config $bootstrapConfig = null ) {
+ if ( self::$instance === null ) {
+ // no global instance yet, nothing to reset
+ return;
+ }
+
+ self::failIfResetNotAllowed( __METHOD__ );
+
+ if ( $bootstrapConfig === null ) {
+ $bootstrapConfig = self::$instance->getBootstrapConfig();
+ }
+
+ self::$instance->destroy();
+
+ self::$instance = self::newInstance( $bootstrapConfig );
+ }
+
+ /**
+ * Creates a new MediaWikiServices instance and initializes it according to the
+ * given $bootstrapConfig. In particular, all wiring files defined in the
+ * ServiceWiringFiles setting are loaded, and the MediaWikiServices hook is called.
+ *
+ * @param Config|null $bootstrapConfig The Config object to be registered as the
+ * 'BootstrapConfig' service. This has to contain at least the information
+ * needed to set up the 'ConfigFactory' service. If not provided, any call
+ * to getBootstrapConfig(), getConfigFactory, or getMainConfig will fail.
+ * A MediaWikiServices instance without access to configuration is called
+ * "primordial".
+ *
+ * @return MediaWikiServices
+ * @throws MWException
+ */
+ private static function newInstance( Config $bootstrapConfig ) {
+ $instance = new self( $bootstrapConfig );
+
+ // Load the default wiring from the specified files.
+ $wiringFiles = $bootstrapConfig->get( 'ServiceWiringFiles' );
+ $instance->loadWiringFiles( $wiringFiles );
+
+ // Provide a traditional hook point to allow extensions to configure services.
+ Hooks::run( 'MediaWikiServices', [ $instance ] );
+
return $instance;
}
+ /**
+ * Disables all storage layer services. After calling this, any attempt to access the
+ * storage layer will result in an error. Use resetGlobalInstance() to restore normal
+ * operation.
+ *
+ * @warning This is intended for extreme situations only and should never be used
+ * while serving normal web requests. Legitimate use cases for this method include
+ * the installation process. Test fixtures may also use this, if the fixture relies
+ * on globalState.
+ *
+ * @see resetGlobalInstance()
+ * @see resetChildProcessServices()
+ */
+ public static function disableStorageBackend() {
+ // TODO: also disable some Caches, JobQueues, etc
+ $destroy = [ 'DBLoadBalancer', 'DBLoadBalancerFactory' ];
+ $services = self::getInstance();
+
+ foreach ( $destroy as $name ) {
+ $services->disableService( $name );
+ }
+ }
+
+ /**
+ * Resets any services that may have become stale after a child process
+ * returns from after pcntl_fork(). It's also safe, but generally unnecessary,
+ * to call this method from the parent process.
+ *
+ * @note This is intended for use in the context of process forking only!
+ *
+ * @see resetGlobalInstance()
+ * @see disableStorageBackend()
+ */
+ public static function resetChildProcessServices() {
+ // NOTE: for now, just reset everything. Since we don't know the interdependencies
+ // between services, we can't do this more selectively at this time.
+ self::resetGlobalInstance();
+
+ // Child, reseed because there is no bug in PHP:
+ // http://bugs.php.net/bug.php?id=42465
+ mt_srand( getmypid() );
+ }
+
+ /**
+ * Resets the given service for testing purposes.
+ *
+ * @warning This is generally unsafe! Other services may still retain references
+ * to the stale service instance, leading to failures and inconsistencies. Subclasses
+ * may use this method to reset specific services under specific instances, but
+ * it should not be exposed to application logic.
+ *
+ * @note With proper dependency injection used throughout the codebase, this method
+ * should not be needed. It is provided to allow tests that pollute global service
+ * instances to clean up.
+ *
+ * @param string $name
+ * @param string $destroy Whether the service instance should be destroyed if it exists.
+ * When set to false, any existing service instance will effectively be detached
+ * from the container.
+ *
+ * @throws MWException if called outside of PHPUnit tests.
+ */
+ public function resetServiceForTesting( $name, $destroy = true ) {
+ if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MW_PARSER_TEST' ) ) {
+ throw new MWException( 'resetServiceForTesting() must not be used outside unit tests.' );
+ }
+
+ $this->resetService( $name, $destroy );
+ }
+
+ /**
+ * Convenience method that throws an exception unless it is called during a phase in which
+ * resetting of global services is allowed. In general, services should not be reset
+ * individually, since that may introduce inconsistencies.
+ *
+ * This method will throw an exception if:
+ *
+ * - self::$resetInProgress is false (to allow all services to be reset together
+ * via resetGlobalInstance)
+ * - and MEDIAWIKI_INSTALL is not defined (to allow services to be reset during installation)
+ * - and MW_PHPUNIT_TEST is not defined (to allow services to be reset during testing)
+ *
+ * This method is intended to be used to safeguard against accidentally resetting
+ * global service instances that are not yet managed by MediaWikiServices. It is
+ * defined here in the MediaWikiServices services class to have a central place
+ * for managing service bootstrapping and resetting.
+ *
+ * @param string $method the name of the caller method, as given by __METHOD__.
+ *
+ * @throws MWException if called outside bootstrap mode.
+ *
+ * @see resetGlobalInstance()
+ * @see forceGlobalInstance()
+ * @see disableStorageBackend()
+ */
+ public static function failIfResetNotAllowed( $method ) {
+ if ( !defined( 'MW_PHPUNIT_TEST' )
+ && !defined( 'MW_PARSER_TEST' )
+ && !defined( 'MEDIAWIKI_INSTALL' )
+ && !defined( 'RUN_MAINTENANCE_IF_MAIN' )
+ && defined( 'MW_SERVICE_BOOTSTRAP_COMPLETE' )
+ ) {
+ throw new MWException( $method . ' may only be called during bootstrapping and unit tests!' );
+ }
+ }
+
/**
* @param Config $config The Config object to be registered as the 'BootstrapConfig' service.
* This has to contain at least the information needed to set up the 'ConfigFactory'
public function __construct( Config $config ) {
parent::__construct();
- // register the given Config object as the bootstrap config service.
+ // Register the given Config object as the bootstrap config service.
$this->defineService( 'BootstrapConfig', function() use ( $config ) {
return $config;
} );
}
+ // CONVENIENCE GETTERS ////////////////////////////////////////////////////
+
/**
* Returns the Config object containing the bootstrap configuration.
* Bootstrap configuration would typically include database credentials
return $this->getService( 'SkinFactory' );
}
+ /**
+ * @return LBFactory
+ */
+ public function getDBLoadBalancerFactory() {
+ return $this->getService( 'DBLoadBalancerFactory' );
+ }
+
+ /**
+ * @return LoadBalancer The main DB load balancer for the local wiki.
+ */
+ public function getDBLoadBalancer() {
+ return $this->getService( 'DBLoadBalancer' );
+ }
+
///////////////////////////////////////////////////////////////////////////
// NOTE: When adding a service getter here, don't forget to add a test
// case for it in MediaWikiServicesTest::provideGetters() and in
use MediaWiki\MediaWikiServices;
return [
+ 'DBLoadBalancerFactory' => function( MediaWikiServices $services ) {
+ $config = $services->getMainConfig()->get( 'LBFactoryConf' );
+
+ $class = LBFactory::getLBFactoryClass( $config );
+ if ( !isset( $config['readOnlyReason'] ) ) {
+ // TODO: replace the global wfConfiguredReadOnlyReason() with a service.
+ $config['readOnlyReason'] = wfConfiguredReadOnlyReason();
+ }
+
+ return new $class( $config );
+ },
+
+ 'DBLoadBalancer' => function( MediaWikiServices $services ) {
+ // just return the default LB from the DBLoadBalancerFactory service
+ return $services->getDBLoadBalancerFactory()->getMainLB();
+ },
+
'SiteStore' => function( MediaWikiServices $services ) {
- $loadBalancer = wfGetLB(); // TODO: use LB from MediaWikiServices
- $rawSiteStore = new DBSiteStore( $loadBalancer );
+ $rawSiteStore = new DBSiteStore( $services->getDBLoadBalancer() );
// TODO: replace wfGetCache with a CacheFactory service.
// TODO: replace wfIsHHVM with a capabilities service.
},
'SkinFactory' => function( MediaWikiServices $services ) {
- return new SkinFactory();
+ $factory = new SkinFactory();
+
+ $names = $services->getMainConfig()->get( 'ValidSkinNames' );
+
+ foreach ( $names as $name => $skin ) {
+ $factory->register( $name, $skin, function () use ( $name, $skin ) {
+ $class = "Skin$skin";
+ return new $class( $name );
+ } );
+ }
+ // Register a hidden "fallback" skin
+ $factory->register( 'fallback', 'Fallback', function () {
+ return new SkinFallback;
+ } );
+ // Register a hidden skin for api output
+ $factory->register( 'apioutput', 'ApiOutput', function () {
+ return new SkinApi;
+ } );
+
+ return $factory;
},
///////////////////////////////////////////////////////////////////////////
--- /dev/null
+<?php
+namespace MediaWiki\Services;
+
+use Exception;
+use RuntimeException;
+
+/**
+ * Exception thrown when trying to replace an already active service.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ *
+ * @since 1.27
+ */
+
+/**
+ * Exception thrown when trying to replace an already active service.
+ */
+class CannotReplaceActiveServiceException extends RuntimeException {
+
+ /**
+ * @param string $serviceName
+ * @param Exception|null $previous
+ */
+ public function __construct( $serviceName, Exception $previous = null ) {
+ parent::__construct( "Cannot replace an active service: $serviceName", 0, $previous );
+ }
+
+}
--- /dev/null
+<?php
+namespace MediaWiki\Services;
+
+use Exception;
+use RuntimeException;
+
+/**
+ * Exception thrown when trying to access a service on a disabled container or factory.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ *
+ * @since 1.27
+ */
+
+/**
+ * Exception thrown when trying to access a service on a disabled container or factory.
+ */
+class ContainerDisabledException extends RuntimeException {
+
+ /**
+ * @param Exception|null $previous
+ */
+ public function __construct( Exception $previous = null ) {
+ parent::__construct( 'Container disabled!', 0, $previous );
+ }
+
+}
--- /dev/null
+<?php
+namespace MediaWiki\Services;
+
+/**
+ * Interface for destructible services.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ *
+ * @since 1.27
+ */
+
+/**
+ * DestructibleService defines a standard interface for shutting down a service instance.
+ * The intended use is for a service container to be able to shut down services that should
+ * no longer be used, and allow such services to release any system resources.
+ *
+ * @note There is no expectation that services will be destroyed when the process (or web request)
+ * terminates.
+ */
+interface DestructibleService {
+
+ /**
+ * Notifies the service object that it should expect to no longer be used, and should release
+ * any system resources it may own. The behavior of all service methods becomes undefined after
+ * destroy() has been called. It is recommended that implementing classes should throw an
+ * exception when service methods are accessed after destroy() has been called.
+ */
+ public function destroy();
+
+}
--- /dev/null
+<?php
+namespace MediaWiki\Services;
+
+use Exception;
+use RuntimeException;
+
+/**
+ * Exception thrown when the requested service is not known.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ *
+ * @since 1.27
+ */
+
+/**
+ * Exception thrown when the requested service is not known.
+ */
+class NoSuchServiceException extends RuntimeException {
+
+ /**
+ * @param string $serviceName
+ * @param Exception|null $previous
+ */
+ public function __construct( $serviceName, Exception $previous = null ) {
+ parent::__construct( "No such service: $serviceName", 0, $previous );
+ }
+
+}
--- /dev/null
+<?php
+namespace MediaWiki\Services;
+
+use Exception;
+use RuntimeException;
+
+/**
+ * Exception thrown when a service was already defined, but the
+ * caller expected it to not exist.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ *
+ * @since 1.27
+ */
+
+/**
+ * Exception thrown when a service was already defined, but the
+ * caller expected it to not exist.
+ */
+class ServiceAlreadyDefinedException extends RuntimeException {
+
+ /**
+ * @param string $serviceName
+ * @param Exception|null $previous
+ */
+ public function __construct( $serviceName, Exception $previous = null ) {
+ parent::__construct( "Service already defined: $serviceName", 0, $previous );
+ }
+
+}
* @see docs/injection.txt for an overview of using dependency injection in the
* MediaWiki code base.
*/
-class ServiceContainer {
+class ServiceContainer implements DestructibleService {
/**
* @var object[]
*/
private $extraInstantiationParams;
+ /**
+ * @var boolean
+ */
+ private $destroyed = false;
+
/**
* @param array $extraInstantiationParams Any additional parameters to be passed to the
* instantiator function when creating a service. This is typically used to provide
$this->extraInstantiationParams = $extraInstantiationParams;
}
+ /**
+ * Destroys all contained service instances that implement the DestructibleService
+ * interface. This will render all services obtained from this MediaWikiServices
+ * instance unusable. In particular, this will disable access to the storage backend
+ * via any of these services. Any future call to getService() will throw an exception.
+ *
+ * @see resetGlobalInstance()
+ */
+ public function destroy() {
+ foreach ( $this->getServiceNames() as $name ) {
+ $service = $this->peekService( $name );
+ if ( $service !== null && $service instanceof DestructibleService ) {
+ $service->destroy();
+ }
+ }
+
+ $this->destroyed = true;
+ }
+
/**
* @param array $wiringFiles A list of PHP files to load wiring information from.
* Each file is loaded using PHP's include mechanism. Each file is expected to
return isset( $this->serviceInstantiators[$name] );
}
+ /**
+ * Returns the service instance for $name only if that service has already been instantiated.
+ * This is intended for situations where services get destroyed/cleaned up, so we can
+ * avoid creating a service just to destroy it again.
+ *
+ * @note This is intended for internal use and for test fixtures.
+ * Application logic should use getService() instead.
+ *
+ * @see getService().
+ *
+ * @param string $name
+ *
+ * @return object|null The service instance, or null if the service has not yet been instantiated.
+ * @throws RuntimeException if $name does not refer to a known service.
+ */
+ public function peekService( $name ) {
+ if ( !$this->hasService( $name ) ) {
+ throw new NoSuchServiceException( $name );
+ }
+
+ return isset( $this->services[$name] ) ? $this->services[$name] : null;
+ }
+
/**
* @return string[]
*/
Assert::parameterType( 'string', $name, '$name' );
if ( $this->hasService( $name ) ) {
- throw new RuntimeException( 'Service already defined: ' . $name );
+ throw new ServiceAlreadyDefinedException( $name );
}
$this->serviceInstantiators[$name] = $instantiator;
Assert::parameterType( 'string', $name, '$name' );
if ( !$this->hasService( $name ) ) {
- throw new RuntimeException( 'Service not defined: ' . $name );
+ throw new NoSuchServiceException( $name );
}
if ( isset( $this->services[$name] ) ) {
- throw new RuntimeException( 'Cannot redefine a service that is already in use: ' . $name );
+ throw new CannotReplaceActiveServiceException( $name );
}
$this->serviceInstantiators[$name] = $instantiator;
}
+ /**
+ * Disables a service.
+ *
+ * @note Attempts to call getService() for a disabled service will result
+ * in a DisabledServiceException. Calling peekService for a disabled service will
+ * return null. Disabled services are listed by getServiceNames(). A disabled service
+ * can be enabled again using redefineService().
+ *
+ * @note If the service was already active (that is, instantiated) when getting disabled,
+ * and the service instance implements DestructibleService, destroy() is called on the
+ * service instance.
+ *
+ * @see redefineService()
+ * @see resetService()
+ *
+ * @param string $name The name of the service to disable.
+ *
+ * @throws RuntimeException if $name is not a known service.
+ */
+ public function disableService( $name ) {
+ $this->resetService( $name );
+
+ $this->redefineService( $name, function() use ( $name ) {
+ throw new ServiceDisabledException( $name );
+ } );
+ }
+
+ /**
+ * Resets a service by dropping the service instance.
+ * If the service instances implements DestructibleService, destroy()
+ * is called on the service instance.
+ *
+ * @warning This is generally unsafe! Other services may still retain references
+ * to the stale service instance, leading to failures and inconsistencies. Subclasses
+ * may use this method to reset specific services under specific instances, but
+ * it should not be exposed to application logic.
+ *
+ * @note This is declared final so subclasses can not interfere with the expectations
+ * disableService() has when calling resetService().
+ *
+ * @see redefineService()
+ * @see disableService().
+ *
+ * @param string $name The name of the service to reset.
+ * @param bool $destroy Whether the service instance should be destroyed if it exists.
+ * When set to false, any existing service instance will effectively be detached
+ * from the container.
+ *
+ * @throws RuntimeException if $name is not a known service.
+ */
+ final protected function resetService( $name, $destroy = true ) {
+ Assert::parameterType( 'string', $name, '$name' );
+
+ $instance = $this->peekService( $name );
+
+ if ( $destroy && $instance instanceof DestructibleService ) {
+ $instance->destroy();
+ }
+
+ unset( $this->services[$name] );
+ }
+
/**
* Returns a service object of the kind associated with $name.
* Services instances are instantiated lazily, on demand.
*
* @param string $name The service name
*
- * @throws InvalidArgumentException if $name is not a known service.
+ * @throws NoSuchServiceException if $name is not a known service.
+ * @throws ServiceDisabledException if this container has already been destroyed.
+ *
* @return object The service instance
*/
public function getService( $name ) {
+ if ( $this->destroyed ) {
+ throw new ContainerDisabledException();
+ }
+
if ( !isset( $this->services[$name] ) ) {
$this->services[$name] = $this->createService( $name );
}
array_merge( [ $this ], $this->extraInstantiationParams )
);
} else {
- throw new InvalidArgumentException( 'Unknown service: ' . $name );
+ throw new NoSuchServiceException( $name );
}
return $service;
--- /dev/null
+<?php
+namespace MediaWiki\Services;
+
+use Exception;
+use RuntimeException;
+
+/**
+ * Exception thrown when trying to access a disabled service.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ *
+ * @since 1.27
+ */
+
+/**
+ * Exception thrown when trying to access a disabled service.
+ */
+class ServiceDisabledException extends RuntimeException {
+
+ /**
+ * @param string $serviceName
+ * @param Exception|null $previous
+ */
+ public function __construct( $serviceName, Exception $previous = null ) {
+ parent::__construct( "Service disabled: $serviceName", 0, $previous );
+ }
+
+}
*
* @file
*/
+use MediaWiki\MediaWikiServices;
/**
* This file is not a valid entry point, perform no further processing unless
$wgSkipSkins[] = $wgSkipSkin;
}
-// Register skins
-// Use a closure to avoid leaking into global state
-call_user_func( function () use ( $wgValidSkinNames ) {
- $factory = SkinFactory::getDefaultInstance();
- foreach ( $wgValidSkinNames as $name => $skin ) {
- $factory->register( $name, $skin, function () use ( $name, $skin ) {
- $class = "Skin$skin";
- return new $class( $name );
- } );
- }
- // Register a hidden "fallback" skin
- $factory->register( 'fallback', 'Fallback', function () {
- return new SkinFallback;
- } );
- // Register a hidden skin for api output
- $factory->register( 'apioutput', 'ApiOutput', function () {
- return new SkinApi;
- } );
-} );
$wgSkipSkins[] = 'fallback';
$wgSkipSkins[] = 'apioutput';
require_once "$IP/includes/AutoLoader.php";
}
+// Reset the global service locator, so any services that have already been created will be
+// re-created while taking into account any custom settings and extensions.
+MediaWikiServices::resetGlobalInstance( new GlobalVarConfig() );
+
+// Define a constant that indicates that the bootstrapping of the service locator
+// is complete.
+define( 'MW_SERVICE_BOOTSTRAP_COMPLETE', 1 );
+
// Install a header callback to prevent caching of responses with cookies (T127993)
if ( !$wgCommandLineMode ) {
header_register_callback( function () {
}
/**
- * Register a new config factory function
- * Will override if it's already registered
+ * @return string[]
+ */
+ public function getConfigNames() {
+ return array_keys( $this->factoryFunctions );
+ }
+
+ /**
+ * Register a new config factory function.
+ * Will override if it's already registered.
+ * Use "*" for $name to provide a fallback config for all unknown names.
* @param string $name
- * @param callable $callback That takes this ConfigFactory as an argument
+ * @param callable|Config $callback A factory callabck that takes this ConfigFactory
+ * as an argument and returns a Config instance, or an existing Config instance.
* @throws InvalidArgumentException If an invalid callback is provided
*/
public function register( $name, $callback ) {
+ if ( $callback instanceof Config ) {
+ $instance = $callback;
+
+ // Register a callback anyway, for consistency. Note that getConfigNames()
+ // relies on $factoryFunctions to have all config names.
+ $callback = function() use ( $instance ) {
+ return $instance;
+ };
+ } else {
+ $instance = null;
+ }
+
if ( !is_callable( $callback ) ) {
throw new InvalidArgumentException( 'Invalid callback provided' );
}
+
+ $this->configs[$name] = $instance;
$this->factoryFunctions[$name] = $callback;
}
*/
public function makeConfig( $name ) {
if ( !isset( $this->configs[$name] ) ) {
- if ( !isset( $this->factoryFunctions[$name] ) ) {
+ $key = $name;
+ if ( !isset( $this->factoryFunctions[$key] ) ) {
+ $key = '*';
+ }
+ if ( !isset( $this->factoryFunctions[$key] ) ) {
throw new ConfigException( "No registered builder available for $name." );
}
- $conf = call_user_func( $this->factoryFunctions[$name], $this );
+ $conf = call_user_func( $this->factoryFunctions[$key], $this );
if ( $conf instanceof Config ) {
$this->configs[$name] = $conf;
} else {
return $this->configs[$name];
}
+
}
* @ingroup Database
*/
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Services\DestructibleService;
use Psr\Log\LoggerInterface;
use MediaWiki\Logger\LoggerFactory;
* An interface for generating database load balancers
* @ingroup Database
*/
-abstract class LBFactory {
+abstract class LBFactory implements DestructibleService {
+
/** @var ChronologyProtector */
protected $chronProt;
/** @var LoggerInterface */
protected $logger;
- /** @var LBFactory */
- private static $instance;
-
/** @var string|bool Reason all LBs are read-only or false if not */
protected $readOnlyReason = false;
$this->logger = LoggerFactory::getInstance( 'DBTransaction' );
}
+ /**
+ * Disables all load balancers. All connections are closed, and any attempt to
+ * open a new connection will result in a DBAccessError.
+ * @see LoadBalancer::disable()
+ */
+ public function destroy() {
+ $this->shutdown();
+ $this->forEachLBCallMethod( 'disable' );
+ }
+
/**
* Disables all access to the load balancer, will cause all database access
* to throw a DBAccessError
*/
public static function disableBackend() {
- global $wgLBFactoryConf;
- self::$instance = new LBFactoryFake( $wgLBFactoryConf );
+ MediaWikiServices::disableStorageBackend();
}
/**
* Get an LBFactory instance
*
+ * @deprecated since 1.27, use MediaWikiServices::getDBLoadBalancerFactory() instead.
+ *
* @return LBFactory
*/
public static function singleton() {
- global $wgLBFactoryConf;
-
- if ( is_null( self::$instance ) ) {
- $class = self::getLBFactoryClass( $wgLBFactoryConf );
- $config = $wgLBFactoryConf;
- if ( !isset( $config['readOnlyReason'] ) ) {
- $config['readOnlyReason'] = wfConfiguredReadOnlyReason();
- }
- self::$instance = new $class( $config );
- }
-
- return self::$instance;
+ return MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
}
/**
* Returns the LBFactory class to use and the load balancer configuration.
*
+ * @todo instead of this, use a ServiceContainer for managing the different implementations.
+ *
* @param array $config (e.g. $wgLBFactoryConf)
* @return string Class name
*/
/**
* Shut down, close connections and destroy the cached instance.
- */
- public static function destroyInstance() {
- if ( self::$instance ) {
- self::$instance->shutdown();
- self::$instance->forEachLBCallMethod( 'closeAll' );
- self::$instance = null;
- }
- }
-
- /**
- * Set the instance to be the given object
*
- * @param LBFactory $instance
+ * @deprecated since 1.27, use LBFactory::destroy()
*/
- public static function setInstance( $instance ) {
- self::destroyInstance();
- self::$instance = $instance;
+ public static function destroyInstance() {
+ self::singleton()->destroy();
}
/**
class DBAccessError extends MWException {
public function __construct() {
parent::__construct( "Mediawiki tried to access the database via wfGetDB(). " .
- "This is not allowed." );
+ "This is not allowed, because database access has been disabled." );
}
}
/** @var integer Max time to wait for a slave to catch up (e.g. ChronologyProtector) */
const POS_WAIT_TIMEOUT = 10;
+ /**
+ * @var boolean
+ */
+ private $disabled = false;
+
/**
* @param array $params Array with keys:
* - servers : Required. Array of server info structures.
* On error, returns false, and the connection which caused the
* error will be available via $this->mErrorConnection.
*
+ * @note If disable() was called on this LoadBalancer, this method will throw a DBAccessError.
+ *
* @param int $i Server index
* @param string|bool $wiki Wiki ID, or false for the current wiki
* @return DatabaseBase|bool Returns false on errors
* On error, returns false, and the connection which caused the
* error will be available via $this->mErrorConnection.
*
+ * @note If disable() was called on this LoadBalancer, this method will throw a DBAccessError.
+ *
* @param int $i Server index
* @param string $wiki Wiki ID to open
* @return DatabaseBase
* @return DatabaseBase
*/
protected function reallyOpenConnection( $server, $dbNameOverride = false ) {
+ if ( $this->disabled ) {
+ throw new DBAccessError();
+ }
+
if ( !is_array( $server ) ) {
throw new MWException( 'You must update your load-balancing configuration. ' .
'See DefaultSettings.php entry for $wgDBservers.' );
return false;
}
+ /**
+ * Disable this load balancer. All connections are closed, and any attempt to
+ * open a new connection will result in a DBAccessError.
+ *
+ * @since 1.27
+ */
+ public function disable() {
+ $this->closeAll();
+ $this->disabled = true;
+ }
+
/**
* Close all open connections
*/
if ( !$status->isOK() ) {
throw new MWException( __METHOD__ . ': unexpected DB connection error' );
}
- LBFactory::setInstance( new LBFactorySingle( [
- 'connection' => $status->value ] ) );
+
+ \MediaWiki\MediaWikiServices::resetGlobalInstance();
+ $services = \MediaWiki\MediaWikiServices::getInstance();
+
+ $connection = $status->value;
+ $services->redefineService( 'DBLoadBalancerFactory', function() use ( $connection ) {
+ return new LBFactorySingle( [
+ 'connection' => $connection ] );
+ } );
+
}
/**
*/
abstract public function showStatusMessage( Status $status );
+ /**
+ * Constructs a Config object that contains configuration settings that should be
+ * overwritten for the installation process.
+ *
+ * @since 1.27
+ *
+ * @param Config $baseConfig
+ *
+ * @return Config The config to use during installation.
+ */
+ public static function getInstallerConfig( Config $baseConfig ) {
+ $configOverrides = new HashConfig();
+
+ // disable (problematic) object cache types explicitly, preserving all other (working) ones
+ // bug T113843
+ $emptyCache = [ 'class' => 'EmptyBagOStuff' ];
+
+ $objectCaches = [
+ CACHE_NONE => $emptyCache,
+ CACHE_DB => $emptyCache,
+ CACHE_ANYTHING => $emptyCache,
+ CACHE_MEMCACHED => $emptyCache,
+ ] + $baseConfig->get( 'ObjectCaches' );
+
+ $configOverrides->set( 'ObjectCaches', $objectCaches );
+
+ // Load the installer's i18n.
+ $messageDirs = $baseConfig->get( 'MessagesDirs' );
+ $messageDirs['MediawikiInstaller'] = __DIR__ . '/i18n';
+
+ $configOverrides->set( 'MessagesDirs', $messageDirs );
+
+ return new MultiConfig( [ $configOverrides, $baseConfig ] );
+ }
+
/**
* Constructor, always call this from child classes.
*/
public function __construct() {
- global $wgMessagesDirs, $wgUser;
+ global $wgMemc, $wgUser;
+
+ $defaultConfig = new GlobalVarConfig(); // all the stuff from DefaultSettings.php
+ $installerConfig = self::getInstallerConfig( $defaultConfig );
+
+ // Reset all services and inject config overrides
+ MediaWiki\MediaWikiServices::resetGlobalInstance( $installerConfig );
// Don't attempt to load user language options (T126177)
// This will be overridden in the web installer with the user-specified language
RequestContext::getMain()->setLanguage( 'en' );
// Disable the i18n cache
+ // TODO: manage LocalisationCache singleton in MediaWikiServices
Language::getLocalisationCache()->disableBackend();
- // Disable LoadBalancer and wfGetDB etc.
- LBFactory::disableBackend();
+
+ // Disable all global services, since we don't have any configuration yet!
+ MediaWiki\MediaWikiServices::disableStorageBackend();
// Disable object cache (otherwise CACHE_ANYTHING will try CACHE_DB and
// SqlBagOStuff will then throw since we just disabled wfGetDB)
- $GLOBALS['wgMemc'] = new EmptyBagOStuff;
- ObjectCache::clear();
- $emptyCache = [ 'class' => 'EmptyBagOStuff' ];
- // disable (problematic) object cache types explicitly, preserving all other (working) ones
- // bug T113843
- $GLOBALS['wgObjectCaches'] = [
- CACHE_NONE => $emptyCache,
- CACHE_DB => $emptyCache,
- CACHE_ANYTHING => $emptyCache,
- CACHE_MEMCACHED => $emptyCache,
- ] + $GLOBALS['wgObjectCaches'];
-
- // Load the installer's i18n.
- $wgMessagesDirs['MediawikiInstaller'] = __DIR__ . '/i18n';
+ $wgMemc = ObjectCache::getInstance( CACHE_NONE );
// Having a user with id = 0 safeguards us from DB access via User::loadOptions().
$wgUser = User::newFromId( 0 );
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
}
}
- 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() );
+ }
+
+ /**
+ * 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 );
+
+ $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;
- // 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;
+ 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();
+ }
+
+ public function run( PHPUnit_Framework_TestResult $result = null ) {
+ // Reset all caches between tests.
+ $this->doLightweightServiceReset();
$needsResetDB = false;
}
$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() !== '' ) {
);
}
+ /**
+ * 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
* @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 ) {
* @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 ) {
*
* @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 ) {
$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
* @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;
$user = User::newFromName( 'UTSysop' );
$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 );
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 ) ) {
<?php
use Liuggio\StatsdClient\Factory\StatsdDataFactory;
use MediaWiki\MediaWikiServices;
+use MediaWiki\Services\ServiceDisabledException;
/**
* @covers MediaWiki\MediaWikiServices
*/
class MediaWikiServicesTest extends PHPUnit_Framework_TestCase {
+ /**
+ * @return Config
+ */
+ private function newTestConfig() {
+ $globalConfig = new GlobalVarConfig();
+
+ $testConfig = new HashConfig();
+ $testConfig->set( 'ServiceWiringFiles', $globalConfig->get( 'ServiceWiringFiles' ) );
+ $testConfig->set( 'ConfigRegistry', $globalConfig->get( 'ConfigRegistry' ) );
+
+ return $testConfig;
+ }
+
+ /**
+ * @return MediaWikiServices
+ */
+ private function newMediaWikiServices( Config $config = null ) {
+ if ( $config === null ) {
+ $config = $this->newTestConfig();
+ }
+
+ $instance = new MediaWikiServices( $config );
+
+ // Load the default wiring from the specified files.
+ $wiringFiles = $config->get( 'ServiceWiringFiles' );
+ $instance->loadWiringFiles( $wiringFiles );
+
+ return $instance;
+ }
+
public function testGetInstance() {
$services = MediaWikiServices::getInstance();
$this->assertInstanceOf( 'MediaWiki\\MediaWikiServices', $services );
}
+ public function testForceGlobalInstance() {
+ $newServices = $this->newMediaWikiServices();
+ $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
+
+ $this->assertInstanceOf( 'MediaWiki\\MediaWikiServices', $oldServices );
+ $this->assertNotSame( $oldServices, $newServices );
+
+ $theServices = MediaWikiServices::getInstance();
+ $this->assertSame( $theServices, $newServices );
+
+ MediaWikiServices::forceGlobalInstance( $oldServices );
+
+ $theServices = MediaWikiServices::getInstance();
+ $this->assertSame( $theServices, $oldServices );
+ }
+
+ public function testResetGlobalInstance() {
+ $newServices = $this->newMediaWikiServices();
+ $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
+
+ MediaWikiServices::resetGlobalInstance( $this->newTestConfig() );
+ $theServices = MediaWikiServices::getInstance();
+
+ $this->assertNotSame( $theServices, $newServices );
+ $this->assertNotSame( $theServices, $oldServices );
+
+ MediaWikiServices::forceGlobalInstance( $oldServices );
+ }
+
+ public function testDisableStorageBackend() {
+ $newServices = $this->newMediaWikiServices();
+ $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
+
+ $lbFactory = $this->getMockBuilder( 'LBFactorySimple' )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $lbFactory->expects( $this->once() )
+ ->method( 'destroy' );
+
+ $newServices->redefineService(
+ 'DBLoadBalancerFactory',
+ function() use ( $lbFactory ) {
+ return $lbFactory;
+ }
+ );
+
+ // force the service to become active, so we can check that it does get destroyed
+ $newServices->getService( 'DBLoadBalancerFactory' );
+
+ MediaWikiServices::disableStorageBackend(); // should destroy DBLoadBalancerFactory
+
+ try {
+ MediaWikiServices::getInstance()->getService( 'DBLoadBalancerFactory' );
+ $this->fail( 'DBLoadBalancerFactory shoudl have been disabled' );
+ }
+ catch ( ServiceDisabledException $ex ) {
+ // ok, as expected
+ }
+ catch ( Throwable $ex ) {
+ $this->fail( 'ServiceDisabledException expected, caught ' . get_class( $ex ) );
+ }
+
+ MediaWikiServices::forceGlobalInstance( $oldServices );
+ }
+
+ public function testResetChildProcessServices() {
+ $newServices = $this->newMediaWikiServices();
+ $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
+
+ $lbFactory = $this->getMockBuilder( 'LBFactorySimple' )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $lbFactory->expects( $this->once() )
+ ->method( 'destroy' );
+
+ $newServices->redefineService(
+ 'DBLoadBalancerFactory',
+ function() use ( $lbFactory ) {
+ return $lbFactory;
+ }
+ );
+
+ // force the service to become active, so we can check that it does get destroyed
+ $oldLBFactory = $newServices->getService( 'DBLoadBalancerFactory' );
+
+ MediaWikiServices::resetChildProcessServices();
+ $finalServices = MediaWikiServices::getInstance();
+
+ $newLBFactory = $finalServices->getService( 'DBLoadBalancerFactory' );
+
+ $this->assertNotSame( $oldLBFactory, $newLBFactory );
+
+ MediaWikiServices::forceGlobalInstance( $oldServices );
+ }
+
+ public function testResetServiceForTesting() {
+ $services = $this->newMediaWikiServices();
+ $serviceCounter = 0;
+
+ $services->defineService(
+ 'Test',
+ function() use ( &$serviceCounter ) {
+ $serviceCounter++;
+ $service = $this->getMock( 'MediaWiki\Services\DestructibleService' );
+ $service->expects( $this->once() )->method( 'destroy' );
+ return $service;
+ }
+ );
+
+ // This should do nothing. In particular, it should not create a service instance.
+ $services->resetServiceForTesting( 'Test' );
+ $this->assertEquals( 0, $serviceCounter, 'No service instance should be created yet.' );
+
+ $oldInstance = $services->getService( 'Test' );
+ $this->assertEquals( 1, $serviceCounter, 'A service instance should exit now.' );
+
+ // The old instance should be detached, and destroy() called.
+ $services->resetServiceForTesting( 'Test' );
+ $newInstance = $services->getService( 'Test' );
+
+ $this->assertNotSame( $oldInstance, $newInstance );
+
+ // Satisfy the expectation that destroy() is called also for the second service instance.
+ $newInstance->destroy();
+ }
+
+ public function testResetServiceForTesting_noDestroy() {
+ $services = $this->newMediaWikiServices();
+
+ $services->defineService(
+ 'Test',
+ function() {
+ $service = $this->getMock( 'MediaWiki\Services\DestructibleService' );
+ $service->expects( $this->never() )->method( 'destroy' );
+ return $service;
+ }
+ );
+
+ $oldInstance = $services->getService( 'Test' );
+
+ // The old instance should be detached, but destroy() not called.
+ $services->resetServiceForTesting( 'Test', false );
+ $newInstance = $services->getService( 'Test' );
+
+ $this->assertNotSame( $oldInstance, $newInstance );
+ }
+
public function provideGetters() {
- // NOTE: This should list all service getters defined in MediaWikiServices.
- // NOTE: For every test case defined here there should be a corresponding
- // test case defined in provideGetService().
- return [
- 'BootstrapConfig' => [ 'getBootstrapConfig', Config::class ],
- 'ConfigFactory' => [ 'getConfigFactory', ConfigFactory::class ],
- 'MainConfig' => [ 'getMainConfig', Config::class ],
- 'SiteStore' => [ 'getSiteStore', SiteStore::class ],
- 'SiteLookup' => [ 'getSiteLookup', SiteLookup::class ],
- 'StatsdDataFactory' => [ 'getStatsdDataFactory', StatsdDataFactory::class ],
- 'EventRelayerGroup' => [ 'getEventRelayerGroup', EventRelayerGroup::class ],
- 'SearchEngine' => [ 'newSearchEngine', SearchEngine::class ],
- 'SearchEngineFactory' => [ 'getSearchEngineFactory', SearchEngineFactory::class ],
- 'SearchEngineConfig' => [ 'getSearchEngineConfig', SearchEngineConfig::class ],
- 'SkinFactory' => [ 'getSkinFactory', SkinFactory::class ],
- ];
+ $getServiceCases = $this->provideGetService();
+ $getterCases = [];
+
+ // All getters should be named just like the service, with "get" added.
+ foreach ( $getServiceCases as $name => $case ) {
+ list( $service, $class ) = $case;
+ $getterCases[$name] = [
+ 'get' . $service,
+ $class,
+ ];
+ }
+
+ return $getterCases;
}
/**
'SearchEngineFactory' => [ 'SearchEngineFactory', SearchEngineFactory::class ],
'SearchEngineConfig' => [ 'SearchEngineConfig', SearchEngineConfig::class ],
'SkinFactory' => [ 'SkinFactory', SkinFactory::class ],
+ 'DBLoadBalancerFactory' => [ 'DBLoadBalancerFactory', 'LBFactory' ],
+ 'DBLoadBalancer' => [ 'DBLoadBalancer', 'LoadBalancer' ],
];
}
$name = 'TestService92834576';
- $this->setExpectedException( 'InvalidArgumentException' );
+ $this->setExpectedException( 'MediaWiki\Services\NoSuchServiceException' );
$services->getService( $name );
}
+ public function testPeekService() {
+ $services = $this->newServiceContainer();
+
+ $services->defineService(
+ 'Foo',
+ function() {
+ return new stdClass();
+ }
+ );
+
+ $services->defineService(
+ 'Bar',
+ function() {
+ return new stdClass();
+ }
+ );
+
+ // trigger instantiation of Foo
+ $services->getService( 'Foo' );
+
+ $this->assertInternalType(
+ 'object',
+ $services->peekService( 'Foo' ),
+ 'Peek should return the service object if it had been accessed before.'
+ );
+
+ $this->assertNull(
+ $services->peekService( 'Bar' ),
+ 'Peek should return null if the service was never accessed.'
+ );
+ }
+
+ public function testPeekService_fail_unknown() {
+ $services = $this->newServiceContainer();
+
+ $name = 'TestService92834576';
+
+ $this->setExpectedException( 'MediaWiki\Services\NoSuchServiceException' );
+
+ $services->peekService( $name );
+ }
+
public function testDefineService() {
$services = $this->newServiceContainer();
return $theService;
} );
- $this->setExpectedException( 'RuntimeException' );
+ $this->setExpectedException( 'MediaWiki\Services\ServiceAlreadyDefinedException' );
$services->defineService( $name, function() use ( $theService ) {
return $theService;
];
// loading the same file twice should fail, because
- $this->setExpectedException( 'RuntimeException' );
+ $this->setExpectedException( 'MediaWiki\Services\ServiceAlreadyDefinedException' );
$services->loadWiringFiles( $wiringFiles );
}
$theService = new stdClass();
$name = 'TestService92834576';
- $this->setExpectedException( 'RuntimeException' );
+ $this->setExpectedException( 'MediaWiki\Services\NoSuchServiceException' );
$services->redefineService( $name, function() use ( $theService ) {
return $theService;
// create the service, so it can no longer be redefined
$services->getService( $name );
- $this->setExpectedException( 'RuntimeException' );
+ $this->setExpectedException( 'MediaWiki\Services\CannotReplaceActiveServiceException' );
+
+ $services->redefineService( $name, function() use ( $theService ) {
+ return $theService;
+ } );
+ }
+
+ public function testDisableService() {
+ $services = $this->newServiceContainer( [ 'Foo' ] );
+
+ $destructible = $this->getMock( 'MediaWiki\Services\DestructibleService' );
+ $destructible->expects( $this->once() )
+ ->method( 'destroy' );
+
+ $services->defineService( 'Foo', function() use ( $destructible ) {
+ return $destructible;
+ } );
+ $services->defineService( 'Bar', function() {
+ return new stdClass();
+ } );
+ $services->defineService( 'Qux', function() {
+ return new stdClass();
+ } );
+
+ // instantiate Foo and Bar services
+ $services->getService( 'Foo' );
+ $services->getService( 'Bar' );
+
+ // disable service, should call destroy() once.
+ $services->disableService( 'Foo' );
+
+ // disabled service should still be listed
+ $this->assertContains( 'Foo', $services->getServiceNames() );
+
+ // getting other services should still work
+ $services->getService( 'Bar' );
+
+ // disable non-destructible service, and not-yet-instantiated service
+ $services->disableService( 'Bar' );
+ $services->disableService( 'Qux' );
+
+ $this->assertNull( $services->peekService( 'Bar' ) );
+ $this->assertNull( $services->peekService( 'Qux' ) );
+
+ // disabled service should still be listed
+ $this->assertContains( 'Bar', $services->getServiceNames() );
+ $this->assertContains( 'Qux', $services->getServiceNames() );
+
+ // re-enable Bar service
+ $services->redefineService( 'Bar', function() {
+ return new stdClass();
+ } );
+
+ $services->getService( 'Bar' );
+
+ $this->setExpectedException( 'MediaWiki\Services\ServiceDisabledException' );
+ $services->getService( 'Qux' );
+ }
+
+ public function testDisableService_fail_undefined() {
+ $services = $this->newServiceContainer();
+
+ $theService = new stdClass();
+ $name = 'TestService92834576';
+
+ $this->setExpectedException( 'MediaWiki\Services\NoSuchServiceException' );
$services->redefineService( $name, function() use ( $theService ) {
return $theService;
} );
}
+ public function testDestroy() {
+ $services = $this->newServiceContainer();
+
+ $destructible = $this->getMock( 'MediaWiki\Services\DestructibleService' );
+ $destructible->expects( $this->once() )
+ ->method( 'destroy' );
+
+ $services->defineService( 'Foo', function() use ( $destructible ) {
+ return $destructible;
+ } );
+
+ $services->defineService( 'Bar', function() {
+ return new stdClass();
+ } );
+
+ // create the service
+ $services->getService( 'Foo' );
+
+ // destroy the container
+ $services->destroy();
+
+ $this->setExpectedException( 'MediaWiki\Services\ContainerDisabledException' );
+ $services->getService( 'Bar' );
+ }
+
}
public function testRegister() {
$factory = new ConfigFactory();
$factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
- $this->assertTrue( true ); // No exception thrown
+ $this->assertInstanceOf( GlobalVarConfig::class, $factory->makeConfig( 'unittest' ) );
+ }
+
+ /**
+ * @covers ConfigFactory::register
+ */
+ public function testRegisterInvalid() {
+ $factory = new ConfigFactory();
$this->setExpectedException( 'InvalidArgumentException' );
$factory->register( 'invalid', 'Invalid callback' );
}
+ /**
+ * @covers ConfigFactory::register
+ */
+ public function testRegisterInstance() {
+ $config = GlobalVarConfig::newInstance();
+ $factory = new ConfigFactory();
+ $factory->register( 'unittest', $config );
+ $this->assertSame( $config, $factory->makeConfig( 'unittest' ) );
+ }
+
+ /**
+ * @covers ConfigFactory::register
+ */
+ public function testRegisterAgain() {
+ $factory = new ConfigFactory();
+ $factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
+ $config1 = $factory->makeConfig( 'unittest' );
+
+ $factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
+ $config2 = $factory->makeConfig( 'unittest' );
+
+ $this->assertNotSame( $config1, $config2 );
+ }
+
+ /**
+ * @covers ConfigFactory::register
+ */
+ public function testGetConfigNames() {
+ $factory = new ConfigFactory();
+ $factory->register( 'foo', 'GlobalVarConfig::newInstance' );
+ $factory->register( 'bar', new HashConfig() );
+
+ $this->assertEquals( [ 'foo', 'bar' ], $factory->getConfigNames() );
+ }
+
/**
* @covers ConfigFactory::makeConfig
*/
public function testMakeConfig() {
$factory = new ConfigFactory();
$factory->register( 'unittest', 'GlobalVarConfig::newInstance' );
+
+ $conf = $factory->makeConfig( 'unittest' );
+ $this->assertInstanceOf( 'Config', $conf );
+ $this->assertSame( $conf, $factory->makeConfig( 'unittest' ) );
+ }
+
+ /**
+ * @covers ConfigFactory::makeConfig
+ */
+ public function testMakeConfigFallback() {
+ $factory = new ConfigFactory();
+ $factory->register( '*', 'GlobalVarConfig::newInstance' );
$conf = $factory->makeConfig( 'unittest' );
$this->assertInstanceOf( 'Config', $conf );
}
* @covers ConfigFactory::getDefaultInstance
*/
public function testGetDefaultInstance() {
+ // NOTE: the global config factory returned here has been overwritten
+ // for operation in test mode. It may not reflect LocalSettings.
$factory = ConfigFactory::getDefaultInstance();
$this->assertInstanceOf( 'Config', $factory->makeConfig( 'main' ) );
-
- $this->setExpectedException( 'ConfigException' );
- $factory->makeConfig( 'xyzzy' );
}
+
}
// may break testing against floating point values
// treated with PHP's serialize()
ini_set( 'serialize_precision', 17 );
+
+ // TODO: we should call MediaWikiTestCase::prepareServices( new GlobalVarConfig() ) here.
+ // But PHPUnit may not be loaded yet, so we have to wait until just
+ // before PHPUnit_TextUI_Command::main() is executed at the end of this file.
}
public function execute() {
'Using HHVM ' . HHVM_VERSION . ' (' . PHP_VERSION . ")\n" :
'Using PHP ' . PHP_VERSION . "\n";
+// Prepare global services for unit tests.
+// FIXME: this should be done in the finalSetup() method,
+// but PHPUnit may not have been loaded at that point.
+MediaWikiTestCase::prepareServices( new GlobalVarConfig() );
+
$wgPhpUnitClass::main();