1<?php 2 3use MediaWiki\Hook\MediaWikiServicesHook; 4use MediaWiki\HookContainer\HookContainer; 5use MediaWiki\HookContainer\StaticHookRegistry; 6use MediaWiki\MediaWikiServices; 7use Wikimedia\Services\DestructibleService; 8use Wikimedia\Services\SalvageableService; 9use Wikimedia\Services\ServiceDisabledException; 10 11/** 12 * @covers MediaWiki\MediaWikiServices 13 */ 14class MediaWikiServicesTest extends MediaWikiIntegrationTestCase { 15 private $deprecatedServices = []; 16 17 public static $mockServiceWiring = []; 18 19 /** 20 * @return Config 21 */ 22 private function newTestConfig() { 23 $globalConfig = new GlobalVarConfig(); 24 25 $testConfig = new HashConfig(); 26 $testConfig->set( 'ServiceWiringFiles', $globalConfig->get( 'ServiceWiringFiles' ) ); 27 $testConfig->set( 'ConfigRegistry', $globalConfig->get( 'ConfigRegistry' ) ); 28 29 return $testConfig; 30 } 31 32 /** 33 * @return MediaWikiServices 34 */ 35 private function newMediaWikiServices() { 36 $config = $this->newTestConfig(); 37 $instance = new MediaWikiServices( $config ); 38 39 // Load the default wiring from the specified files. 40 $wiringFiles = $config->get( 'ServiceWiringFiles' ); 41 $instance->loadWiringFiles( $wiringFiles ); 42 43 return $instance; 44 } 45 46 private function newConfigWithMockWiring() { 47 $config = new HashConfig; 48 $config->set( 'ServiceWiringFiles', [ __DIR__ . '/MockServiceWiring.php' ] ); 49 return $config; 50 } 51 52 public function testGetInstance() { 53 $services = MediaWikiServices::getInstance(); 54 $this->assertInstanceOf( MediaWikiServices::class, $services ); 55 } 56 57 public function testForceGlobalInstance() { 58 $newServices = $this->newMediaWikiServices(); 59 $oldServices = MediaWikiServices::forceGlobalInstance( $newServices ); 60 61 $this->assertInstanceOf( MediaWikiServices::class, $oldServices ); 62 $this->assertNotSame( $oldServices, $newServices ); 63 64 $theServices = MediaWikiServices::getInstance(); 65 $this->assertSame( $theServices, $newServices ); 66 67 MediaWikiServices::forceGlobalInstance( $oldServices ); 68 69 $theServices = MediaWikiServices::getInstance(); 70 $this->assertSame( $theServices, $oldServices ); 71 } 72 73 public function testResetGlobalInstance() { 74 $newServices = $this->newMediaWikiServices(); 75 $oldServices = MediaWikiServices::forceGlobalInstance( $newServices ); 76 77 $service1 = $this->createMock( SalvageableService::class ); 78 $service1->expects( $this->never() ) 79 ->method( 'salvage' ); 80 81 $newServices->defineService( 82 'Test', 83 static function () use ( $service1 ) { 84 return $service1; 85 } 86 ); 87 88 // force instantiation 89 $newServices->getService( 'Test' ); 90 91 MediaWikiServices::resetGlobalInstance( $this->newTestConfig() ); 92 $theServices = MediaWikiServices::getInstance(); 93 94 $this->assertSame( 95 $service1, 96 $theServices->getService( 'Test' ), 97 'service definition should survive reset' 98 ); 99 100 $this->assertNotSame( $theServices, $newServices ); 101 $this->assertNotSame( $theServices, $oldServices ); 102 103 MediaWikiServices::forceGlobalInstance( $oldServices ); 104 } 105 106 public function testResetGlobalInstance_quick() { 107 $newServices = $this->newMediaWikiServices(); 108 $oldServices = MediaWikiServices::forceGlobalInstance( $newServices ); 109 110 $service1 = $this->createMock( SalvageableService::class ); 111 $service1->expects( $this->never() ) 112 ->method( 'salvage' ); 113 114 $service2 = $this->createMock( SalvageableService::class ); 115 $service2->expects( $this->once() ) 116 ->method( 'salvage' ) 117 ->with( $service1 ); 118 119 // sequence of values the instantiator will return 120 $instantiatorReturnValues = [ 121 $service1, 122 $service2, 123 ]; 124 125 $newServices->defineService( 126 'Test', 127 static function () use ( &$instantiatorReturnValues ) { 128 return array_shift( $instantiatorReturnValues ); 129 } 130 ); 131 132 // force instantiation 133 $newServices->getService( 'Test' ); 134 135 MediaWikiServices::resetGlobalInstance( $this->newTestConfig(), 'quick' ); 136 $theServices = MediaWikiServices::getInstance(); 137 138 $this->assertSame( $service2, $theServices->getService( 'Test' ) ); 139 140 $this->assertNotSame( $theServices, $newServices ); 141 $this->assertNotSame( $theServices, $oldServices ); 142 143 MediaWikiServices::forceGlobalInstance( $oldServices ); 144 } 145 146 public function testResetGlobalInstance_T263925() { 147 $newServices = $this->newMediaWikiServices(); 148 $oldServices = MediaWikiServices::forceGlobalInstance( $newServices ); 149 self::$mockServiceWiring = [ 150 'HookContainer' => function ( MediaWikiServices $services ) { 151 return new HookContainer( 152 new StaticHookRegistry( 153 [], 154 [ 155 'MediaWikiServices' => [ 156 [ 157 'handler' => [ 158 'name' => 'test', 159 'factory' => static function () { 160 return new class implements MediaWikiServicesHook { 161 public function onMediaWikiServices( $services ) { 162 } 163 }; 164 } 165 ], 166 'deprecated' => false, 167 'extensionPath' => 'path' 168 ], 169 ] 170 ], 171 [] 172 ), 173 $this->createSimpleObjectFactory() 174 ); 175 } 176 ]; 177 $newServices->redefineService( 'HookContainer', 178 self::$mockServiceWiring['HookContainer'] ); 179 180 $newServices->getHookContainer()->run( 'MediaWikiServices', [ $newServices ] ); 181 MediaWikiServices::resetGlobalInstance( $this->newConfigWithMockWiring(), 'quick' ); 182 $this->assertTrue( true, 'expected no exception from above' ); 183 184 self::$mockServiceWiring = []; 185 MediaWikiServices::forceGlobalInstance( $oldServices ); 186 } 187 188 public function testDisableStorageBackend() { 189 $newServices = $this->newMediaWikiServices(); 190 $oldServices = MediaWikiServices::forceGlobalInstance( $newServices ); 191 192 $lbFactory = $this->getMockBuilder( \Wikimedia\Rdbms\LBFactorySimple::class ) 193 ->disableOriginalConstructor() 194 ->getMock(); 195 196 $newServices->redefineService( 197 'DBLoadBalancerFactory', 198 static function () use ( $lbFactory ) { 199 return $lbFactory; 200 } 201 ); 202 203 // force the service to become active, so we can check that it does get destroyed 204 $newServices->getService( 'DBLoadBalancerFactory' ); 205 206 MediaWikiServices::disableStorageBackend(); // should destroy DBLoadBalancerFactory 207 208 try { 209 MediaWikiServices::getInstance()->getService( 'DBLoadBalancerFactory' ); 210 $this->fail( 'DBLoadBalancerFactory should have been disabled' ); 211 } 212 catch ( ServiceDisabledException $ex ) { 213 // ok, as expected 214 } catch ( Throwable $ex ) { 215 $this->fail( 'ServiceDisabledException expected, caught ' . get_class( $ex ) ); 216 } 217 218 MediaWikiServices::forceGlobalInstance( $oldServices ); 219 $newServices->destroy(); 220 221 // No exception was thrown, avoid being risky 222 $this->assertTrue( true ); 223 } 224 225 public function testResetChildProcessServices() { 226 $newServices = $this->newMediaWikiServices(); 227 $oldServices = MediaWikiServices::forceGlobalInstance( $newServices ); 228 229 $service1 = $this->createMock( DestructibleService::class ); 230 $service1->expects( $this->once() ) 231 ->method( 'destroy' ); 232 233 $service2 = $this->createMock( DestructibleService::class ); 234 $service2->expects( $this->never() ) 235 ->method( 'destroy' ); 236 237 // sequence of values the instantiator will return 238 $instantiatorReturnValues = [ 239 $service1, 240 $service2, 241 ]; 242 243 $newServices->defineService( 244 'Test', 245 static function () use ( &$instantiatorReturnValues ) { 246 return array_shift( $instantiatorReturnValues ); 247 } 248 ); 249 250 // force the service to become active, so we can check that it does get destroyed 251 $oldTestService = $newServices->getService( 'Test' ); 252 253 MediaWikiServices::resetChildProcessServices(); 254 $finalServices = MediaWikiServices::getInstance(); 255 256 $newTestService = $finalServices->getService( 'Test' ); 257 $this->assertNotSame( $oldTestService, $newTestService ); 258 259 MediaWikiServices::forceGlobalInstance( $oldServices ); 260 } 261 262 public function testResetServiceForTesting() { 263 $services = $this->newMediaWikiServices(); 264 $serviceCounter = 0; 265 266 $services->defineService( 267 'Test', 268 function () use ( &$serviceCounter ) { 269 $serviceCounter++; 270 $service = $this->createMock( Wikimedia\Services\DestructibleService::class ); 271 $service->expects( $this->once() )->method( 'destroy' ); 272 return $service; 273 } 274 ); 275 276 // This should do nothing. In particular, it should not create a service instance. 277 $services->resetServiceForTesting( 'Test' ); 278 $this->assertSame( 0, $serviceCounter, 'No service instance should be created yet.' ); 279 280 $oldInstance = $services->getService( 'Test' ); 281 $this->assertSame( 1, $serviceCounter, 'A service instance should exit now.' ); 282 283 // The old instance should be detached, and destroy() called. 284 $services->resetServiceForTesting( 'Test' ); 285 $newInstance = $services->getService( 'Test' ); 286 287 $this->assertNotSame( $oldInstance, $newInstance ); 288 289 // Satisfy the expectation that destroy() is called also for the second service instance. 290 $newInstance->destroy(); 291 } 292 293 public function testResetServiceForTesting_noDestroy() { 294 $services = $this->newMediaWikiServices(); 295 296 $services->defineService( 297 'Test', 298 function () { 299 $service = $this->createMock( Wikimedia\Services\DestructibleService::class ); 300 $service->expects( $this->never() )->method( 'destroy' ); 301 return $service; 302 } 303 ); 304 305 $oldInstance = $services->getService( 'Test' ); 306 307 // The old instance should be detached, but destroy() not called. 308 $services->resetServiceForTesting( 'Test', false ); 309 $newInstance = $services->getService( 'Test' ); 310 311 $this->assertNotSame( $oldInstance, $newInstance ); 312 } 313 314 public function provideGetters() { 315 $getServiceCases = $this->provideGetService(); 316 $getterCases = []; 317 318 // All getters should be named just like the service, with "get" added. 319 foreach ( $getServiceCases as $name => $case ) { 320 if ( $name[0] === '_' ) { 321 // Internal service, no getter 322 continue; 323 } 324 list( $service, $class ) = $case; 325 $getterCases[$name] = [ 326 'get' . $service, 327 $class, 328 in_array( $service, $this->deprecatedServices ) 329 ]; 330 } 331 332 return $getterCases; 333 } 334 335 /** 336 * @dataProvider provideGetters 337 */ 338 public function testGetters( $getter, $type, $isDeprecated = false ) { 339 if ( $isDeprecated ) { 340 $this->hideDeprecated( MediaWikiServices::class . "::$getter" ); 341 } 342 343 // Test against the default instance, since the dummy will not know the default services. 344 $services = MediaWikiServices::getInstance(); 345 $service = $services->$getter(); 346 $this->assertInstanceOf( $type, $service ); 347 } 348 349 public function provideGetService() { 350 global $IP; 351 $serviceList = require "$IP/includes/ServiceWiring.php"; 352 $ret = []; 353 foreach ( $serviceList as $name => $callback ) { 354 $fun = new ReflectionFunction( $callback ); 355 if ( !$fun->hasReturnType() ) { 356 throw new MWException( 'All service callbacks must have a return type defined, ' . 357 "none found for $name" ); 358 } 359 360 $returnType = $fun->getReturnType(); 361 $ret[$name] = [ $name, $returnType->getName() ]; 362 } 363 return $ret; 364 } 365 366 /** 367 * @dataProvider provideGetService 368 */ 369 public function testGetService( $name, $type ) { 370 // Test against the default instance, since the dummy will not know the default services. 371 $services = MediaWikiServices::getInstance(); 372 373 $service = $services->getService( $name ); 374 $this->assertInstanceOf( $type, $service ); 375 } 376 377 public function testDefaultServiceInstantiation() { 378 // Check all services in the default instance, not a dummy instance! 379 // Note that we instantiate all services here, including any that 380 // were registered by extensions. 381 $services = MediaWikiServices::getInstance(); 382 $names = $services->getServiceNames(); 383 384 foreach ( $names as $name ) { 385 $this->assertTrue( $services->hasService( $name ) ); 386 $service = $services->getService( $name ); 387 $this->assertIsObject( $service ); 388 } 389 } 390 391 public function testDefaultServiceWiringServicesHaveTests() { 392 global $IP; 393 $testedServices = array_keys( $this->provideGetService() ); 394 $allServices = array_keys( require "$IP/includes/ServiceWiring.php" ); 395 $this->assertEquals( 396 [], 397 array_diff( $allServices, $testedServices ), 398 'The following services have not been added to MediaWikiServicesTest::provideGetService' 399 ); 400 } 401 402 public function testGettersAreSorted() { 403 $methods = ( new ReflectionClass( MediaWikiServices::class ) ) 404 ->getMethods( ReflectionMethod::IS_STATIC | ReflectionMethod::IS_PUBLIC ); 405 406 $names = array_map( static function ( $method ) { 407 return $method->getName(); 408 }, $methods ); 409 $serviceNames = array_map( static function ( $name ) { 410 return "get$name"; 411 }, array_keys( $this->provideGetService() ) ); 412 $names = array_values( array_filter( $names, static function ( $name ) use ( $serviceNames ) { 413 return in_array( $name, $serviceNames ); 414 } ) ); 415 416 $sortedNames = $names; 417 natcasesort( $sortedNames ); 418 419 $this->assertSame( $sortedNames, $names, 420 'Please keep service getters sorted alphabetically' ); 421 } 422} 423