3 use MediaWiki\Config\Config
;
4 use MediaWiki\Config\GlobalVarConfig
;
5 use MediaWiki\Config\HashConfig
;
6 use MediaWiki\Hook\MediaWikiServicesHook
;
7 use MediaWiki\HookContainer\HookContainer
;
8 use MediaWiki\HookContainer\StaticHookRegistry
;
9 use MediaWiki\MainConfigNames
;
10 use MediaWiki\MediaWikiServices
;
11 use Wikimedia\Services\DestructibleService
;
12 use Wikimedia\Services\SalvageableService
;
15 * @covers \MediaWiki\MediaWikiServices
17 * This test doesn't really make queries, but needs to be in the Database test to make sure
18 * that storage isn't disabled on the original instance.
20 class MediaWikiServicesTest
extends MediaWikiIntegrationTestCase
{
21 private const DEPRECATED_SERVICES
= [
22 'BlockErrorFormatter',
24 'ConfiguredReadOnlyMode',
28 public static $mockServiceWiring = [];
33 private function newTestConfig() {
34 $globalConfig = new GlobalVarConfig();
36 $testConfig = new HashConfig();
37 $testConfig->set( MainConfigNames
::ServiceWiringFiles
, $globalConfig->get( MainConfigNames
::ServiceWiringFiles
) );
38 $testConfig->set( MainConfigNames
::ConfigRegistry
, $globalConfig->get( MainConfigNames
::ConfigRegistry
) );
39 $testConfig->set( MainConfigNames
::Hooks
, [] );
45 * @return MediaWikiServices
47 private function newMediaWikiServices() {
48 $config = $this->newTestConfig();
49 $instance = new MediaWikiServices( $config );
51 // Load the default wiring from the specified files.
52 $wiringFiles = $config->get( MainConfigNames
::ServiceWiringFiles
);
53 $instance->loadWiringFiles( $wiringFiles );
58 private function newConfigWithMockWiring() {
59 $config = new HashConfig
;
60 $config->set( MainConfigNames
::ServiceWiringFiles
, [ __DIR__
. '/MockServiceWiring.php' ] );
64 public function testGetInstance() {
65 $services = MediaWikiServices
::getInstance();
66 $this->assertInstanceOf( MediaWikiServices
::class, $services );
69 public function testForceGlobalInstance() {
70 $newServices = $this->newMediaWikiServices();
71 $oldServices = MediaWikiServices
::forceGlobalInstance( $newServices );
73 $this->assertInstanceOf( MediaWikiServices
::class, $oldServices );
74 $this->assertNotSame( $oldServices, $newServices );
76 $theServices = MediaWikiServices
::getInstance();
77 $this->assertSame( $theServices, $newServices );
79 MediaWikiServices
::forceGlobalInstance( $oldServices );
81 $theServices = MediaWikiServices
::getInstance();
82 $this->assertSame( $theServices, $oldServices );
85 public function testResetGlobalInstance() {
86 $newServices = $this->newMediaWikiServices();
87 $oldServices = MediaWikiServices
::forceGlobalInstance( $newServices );
89 $service1 = $this->createMock( SalvageableService
::class );
90 $service1->expects( $this->never() )
91 ->method( 'salvage' );
93 $newServices->defineService(
95 static function () use ( $service1 ) {
100 // force instantiation
101 $newServices->getService( 'Test' );
103 MediaWikiServices
::resetGlobalInstance( $this->newTestConfig() );
104 $theServices = MediaWikiServices
::getInstance();
108 $theServices->getService( 'Test' ),
109 'service definition should survive reset'
112 $this->assertNotSame( $theServices, $newServices );
113 $this->assertNotSame( $theServices, $oldServices );
115 MediaWikiServices
::forceGlobalInstance( $oldServices );
118 public function testResetGlobalInstance_quick() {
119 $newServices = $this->newMediaWikiServices();
120 $oldServices = MediaWikiServices
::forceGlobalInstance( $newServices );
122 $service1 = $this->createMock( SalvageableService
::class );
123 $service1->expects( $this->never() )
124 ->method( 'salvage' );
126 $service2 = $this->createMock( SalvageableService
::class );
127 $service2->expects( $this->once() )
128 ->method( 'salvage' )
131 // sequence of values the instantiator will return
132 $instantiatorReturnValues = [
137 $newServices->defineService(
139 static function () use ( &$instantiatorReturnValues ) {
140 return array_shift( $instantiatorReturnValues );
144 // force instantiation
145 $newServices->getService( 'Test' );
147 MediaWikiServices
::resetGlobalInstance( $this->newTestConfig(), 'quick' );
148 $theServices = MediaWikiServices
::getInstance();
150 $this->assertSame( $service2, $theServices->getService( 'Test' ) );
152 $this->assertNotSame( $theServices, $newServices );
153 $this->assertNotSame( $theServices, $oldServices );
155 MediaWikiServices
::forceGlobalInstance( $oldServices );
158 public function testResetGlobalInstance_T263925() {
159 $newServices = $this->newMediaWikiServices();
160 $oldServices = MediaWikiServices
::forceGlobalInstance( $newServices );
161 self
::$mockServiceWiring = [
162 'HookContainer' => function ( MediaWikiServices
$services ) {
163 return new HookContainer(
164 new StaticHookRegistry(
167 'MediaWikiServices' => [
171 'factory' => static function () {
172 return new class implements MediaWikiServicesHook
{
173 public function onMediaWikiServices( $services ) {
178 'deprecated' => false,
179 'extensionPath' => 'path'
185 $this->createSimpleObjectFactory()
189 $newServices->redefineService( 'HookContainer',
190 self
::$mockServiceWiring['HookContainer'] );
192 $newServices->getHookContainer()->run( 'MediaWikiServices', [ $newServices ] );
193 MediaWikiServices
::resetGlobalInstance( $this->newConfigWithMockWiring(), 'quick' );
194 $this->assertTrue( true, 'expected no exception from above' );
196 self
::$mockServiceWiring = [];
197 MediaWikiServices
::forceGlobalInstance( $oldServices );
200 public function testDisableStorage() {
201 $newServices = $this->newMediaWikiServices();
202 $oldServices = MediaWikiServices
::forceGlobalInstance( $newServices );
204 $lbFactory = $this->createMock( \Wikimedia\Rdbms\LBFactorySimple
::class );
206 $newServices->redefineService(
207 'DBLoadBalancerFactory',
208 static function () use ( $lbFactory ) {
213 $this->assertFalse( $newServices->isStorageDisabled() );
215 $newServices->disableStorage(); // should destroy DBLoadBalancerFactory
217 $this->assertTrue( $newServices->isStorageDisabled() );
220 $newServices->getDBLoadBalancer()->getConnection( DB_REPLICA
);
221 } catch ( RuntimeException
$ex ) {
225 MediaWikiServices
::forceGlobalInstance( $oldServices );
226 $newServices->destroy();
228 // This should work now.
229 MediaWikiServices
::getInstance()->getDBLoadBalancer()->getConnection( DB_REPLICA
);
231 // No exception was thrown, avoid being risky
232 $this->assertTrue( true );
235 public function testResetChildProcessServices() {
236 $newServices = $this->newMediaWikiServices();
237 $oldServices = MediaWikiServices
::forceGlobalInstance( $newServices );
239 $service1 = $this->createMock( DestructibleService
::class );
240 $service1->expects( $this->once() )
241 ->method( 'destroy' );
243 $service2 = $this->createMock( DestructibleService
::class );
244 $service2->expects( $this->never() )
245 ->method( 'destroy' );
247 // sequence of values the instantiator will return
248 $instantiatorReturnValues = [
253 $newServices->defineService(
255 static function () use ( &$instantiatorReturnValues ) {
256 return array_shift( $instantiatorReturnValues );
260 // force the service to become active, so we can check that it does get destroyed
261 $oldTestService = $newServices->getService( 'Test' );
263 MediaWikiServices
::resetChildProcessServices();
264 $finalServices = MediaWikiServices
::getInstance();
266 $newTestService = $finalServices->getService( 'Test' );
267 $this->assertNotSame( $oldTestService, $newTestService );
269 MediaWikiServices
::forceGlobalInstance( $oldServices );
272 public function testResetServiceForTesting() {
273 $services = $this->newMediaWikiServices();
276 $services->defineService(
278 function () use ( &$serviceCounter ) {
280 $service = $this->createMock( Wikimedia\Services\DestructibleService
::class );
281 $service->expects( $this->once() )->method( 'destroy' );
286 // This should do nothing. In particular, it should not create a service instance.
287 $services->resetServiceForTesting( 'Test' );
288 $this->assertSame( 0, $serviceCounter, 'No service instance should be created yet.' );
290 $oldInstance = $services->getService( 'Test' );
291 $this->assertSame( 1, $serviceCounter, 'A service instance should exit now.' );
293 // The old instance should be detached, and destroy() called.
294 $services->resetServiceForTesting( 'Test' );
295 $newInstance = $services->getService( 'Test' );
297 $this->assertNotSame( $oldInstance, $newInstance );
299 // Satisfy the expectation that destroy() is called also for the second service instance.
300 $newInstance->destroy();
303 public function testResetServiceForTesting_noDestroy() {
304 $services = $this->newMediaWikiServices();
306 $services->defineService(
309 $service = $this->createMock( Wikimedia\Services\DestructibleService
::class );
310 $service->expects( $this->never() )->method( 'destroy' );
315 $oldInstance = $services->getService( 'Test' );
317 // The old instance should be detached, but destroy() not called.
318 $services->resetServiceForTesting( 'Test', false );
319 $newInstance = $services->getService( 'Test' );
321 $this->assertNotSame( $oldInstance, $newInstance );
324 public function provideGetters() {
325 $getServiceCases = self
::provideGetService();
328 // All getters should be named just like the service, with "get" added.
329 foreach ( $getServiceCases as $name => $case ) {
330 if ( $name[0] === '_' ) {
331 // Internal service, no getter
334 [ $service, $class ] = $case;
335 $getterCases[$name] = [
338 in_array( $service, self
::DEPRECATED_SERVICES
)
346 * @dataProvider provideGetters
348 public function testGetters( $getter, $type, $isDeprecated = false ) {
349 if ( $isDeprecated ) {
350 $this->hideDeprecated( MediaWikiServices
::class . "::$getter" );
353 // Test against the default instance, since the dummy will not know the default services.
354 $services = MediaWikiServices
::getInstance();
355 $service = $services->$getter();
356 $this->assertInstanceOf( $type, $service );
359 public static function provideGetService() {
361 $serviceList = require "$IP/includes/ServiceWiring.php";
363 foreach ( $serviceList as $name => $callback ) {
364 $fun = new ReflectionFunction( $callback );
365 if ( !$fun->hasReturnType() ) {
366 throw new LogicException( 'All service callbacks must have a return type defined, ' .
367 "none found for $name" );
370 $returnType = $fun->getReturnType();
371 $ret[$name] = [ $name, $returnType->getName() ];
377 * @dataProvider provideGetService
379 public function testGetService( $name, $type ) {
380 // Test against the default instance, since the dummy will not know the default services.
381 $services = MediaWikiServices
::getInstance();
383 $service = $services->getService( $name );
384 $this->assertInstanceOf( $type, $service );
387 public function testDefaultServiceInstantiation() {
388 // Check all services in the default instance, not a dummy instance!
389 // Note that we instantiate all services here, including any that
390 // were registered by extensions.
391 $services = MediaWikiServices
::getInstance();
392 $names = $services->getServiceNames();
394 foreach ( $names as $name ) {
395 $this->assertTrue( $services->hasService( $name ) );
396 $service = $services->getService( $name );
397 $this->assertIsObject( $service );
401 public function testDefaultServiceWiringServicesHaveTests() {
403 $testedServices = array_keys( self
::provideGetService() );
404 $allServices = array_keys( require "$IP/includes/ServiceWiring.php" );
407 array_diff( $allServices, $testedServices ),
408 'The following services have not been added to MediaWikiServicesTest::provideGetService'
412 public function testGettersAreSorted() {
413 $methods = ( new ReflectionClass( MediaWikiServices
::class ) )
414 ->getMethods( ReflectionMethod
::IS_STATIC | ReflectionMethod
::IS_PUBLIC
);
416 $names = array_map( static function ( $method ) {
417 return $method->getName();
419 $serviceNames = array_map( static function ( $name ) {
421 }, array_keys( self
::provideGetService() ) );
422 $names = array_values( array_filter( $names, static function ( $name ) use ( $serviceNames ) {
423 return in_array( $name, $serviceNames );
426 $sortedNames = $names;
427 natcasesort( $sortedNames );
429 $this->assertSame( $sortedNames, $names,
430 'Please keep service getters sorted alphabetically' );