MediaWikiServicesTest.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. <?php
  2. use MediaWiki\MediaWikiServices;
  3. use MediaWiki\Services\DestructibleService;
  4. use MediaWiki\Services\SalvageableService;
  5. use MediaWiki\Services\ServiceDisabledException;
  6. /**
  7. * @covers MediaWiki\MediaWikiServices
  8. *
  9. * @group MediaWiki
  10. */
  11. class MediaWikiServicesTest extends MediaWikiTestCase {
  12. /**
  13. * @return Config
  14. */
  15. private function newTestConfig() {
  16. $globalConfig = new GlobalVarConfig();
  17. $testConfig = new HashConfig();
  18. $testConfig->set( 'ServiceWiringFiles', $globalConfig->get( 'ServiceWiringFiles' ) );
  19. $testConfig->set( 'ConfigRegistry', $globalConfig->get( 'ConfigRegistry' ) );
  20. return $testConfig;
  21. }
  22. /**
  23. * @return MediaWikiServices
  24. */
  25. private function newMediaWikiServices( Config $config = null ) {
  26. if ( $config === null ) {
  27. $config = $this->newTestConfig();
  28. }
  29. $instance = new MediaWikiServices( $config );
  30. // Load the default wiring from the specified files.
  31. $wiringFiles = $config->get( 'ServiceWiringFiles' );
  32. $instance->loadWiringFiles( $wiringFiles );
  33. return $instance;
  34. }
  35. public function testGetInstance() {
  36. $services = MediaWikiServices::getInstance();
  37. $this->assertInstanceOf( MediaWikiServices::class, $services );
  38. }
  39. public function testForceGlobalInstance() {
  40. $newServices = $this->newMediaWikiServices();
  41. $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
  42. $this->assertInstanceOf( MediaWikiServices::class, $oldServices );
  43. $this->assertNotSame( $oldServices, $newServices );
  44. $theServices = MediaWikiServices::getInstance();
  45. $this->assertSame( $theServices, $newServices );
  46. MediaWikiServices::forceGlobalInstance( $oldServices );
  47. $theServices = MediaWikiServices::getInstance();
  48. $this->assertSame( $theServices, $oldServices );
  49. }
  50. public function testResetGlobalInstance() {
  51. $newServices = $this->newMediaWikiServices();
  52. $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
  53. $service1 = $this->createMock( SalvageableService::class );
  54. $service1->expects( $this->never() )
  55. ->method( 'salvage' );
  56. $newServices->defineService(
  57. 'Test',
  58. function () use ( $service1 ) {
  59. return $service1;
  60. }
  61. );
  62. // force instantiation
  63. $newServices->getService( 'Test' );
  64. MediaWikiServices::resetGlobalInstance( $this->newTestConfig() );
  65. $theServices = MediaWikiServices::getInstance();
  66. $this->assertSame(
  67. $service1,
  68. $theServices->getService( 'Test' ),
  69. 'service definition should survive reset'
  70. );
  71. $this->assertNotSame( $theServices, $newServices );
  72. $this->assertNotSame( $theServices, $oldServices );
  73. MediaWikiServices::forceGlobalInstance( $oldServices );
  74. }
  75. public function testResetGlobalInstance_quick() {
  76. $newServices = $this->newMediaWikiServices();
  77. $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
  78. $service1 = $this->createMock( SalvageableService::class );
  79. $service1->expects( $this->never() )
  80. ->method( 'salvage' );
  81. $service2 = $this->createMock( SalvageableService::class );
  82. $service2->expects( $this->once() )
  83. ->method( 'salvage' )
  84. ->with( $service1 );
  85. // sequence of values the instantiator will return
  86. $instantiatorReturnValues = [
  87. $service1,
  88. $service2,
  89. ];
  90. $newServices->defineService(
  91. 'Test',
  92. function () use ( &$instantiatorReturnValues ) {
  93. return array_shift( $instantiatorReturnValues );
  94. }
  95. );
  96. // force instantiation
  97. $newServices->getService( 'Test' );
  98. MediaWikiServices::resetGlobalInstance( $this->newTestConfig(), 'quick' );
  99. $theServices = MediaWikiServices::getInstance();
  100. $this->assertSame( $service2, $theServices->getService( 'Test' ) );
  101. $this->assertNotSame( $theServices, $newServices );
  102. $this->assertNotSame( $theServices, $oldServices );
  103. MediaWikiServices::forceGlobalInstance( $oldServices );
  104. }
  105. public function testDisableStorageBackend() {
  106. $newServices = $this->newMediaWikiServices();
  107. $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
  108. $lbFactory = $this->getMockBuilder( \Wikimedia\Rdbms\LBFactorySimple::class )
  109. ->disableOriginalConstructor()
  110. ->getMock();
  111. $newServices->redefineService(
  112. 'DBLoadBalancerFactory',
  113. function () use ( $lbFactory ) {
  114. return $lbFactory;
  115. }
  116. );
  117. // force the service to become active, so we can check that it does get destroyed
  118. $newServices->getService( 'DBLoadBalancerFactory' );
  119. MediaWikiServices::disableStorageBackend(); // should destroy DBLoadBalancerFactory
  120. try {
  121. MediaWikiServices::getInstance()->getService( 'DBLoadBalancerFactory' );
  122. $this->fail( 'DBLoadBalancerFactory should have been disabled' );
  123. }
  124. catch ( ServiceDisabledException $ex ) {
  125. // ok, as expected
  126. } catch ( Throwable $ex ) {
  127. $this->fail( 'ServiceDisabledException expected, caught ' . get_class( $ex ) );
  128. }
  129. MediaWikiServices::forceGlobalInstance( $oldServices );
  130. $newServices->destroy();
  131. // No exception was thrown, avoid being risky
  132. $this->assertTrue( true );
  133. }
  134. public function testResetChildProcessServices() {
  135. $newServices = $this->newMediaWikiServices();
  136. $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
  137. $service1 = $this->createMock( DestructibleService::class );
  138. $service1->expects( $this->once() )
  139. ->method( 'destroy' );
  140. $service2 = $this->createMock( DestructibleService::class );
  141. $service2->expects( $this->never() )
  142. ->method( 'destroy' );
  143. // sequence of values the instantiator will return
  144. $instantiatorReturnValues = [
  145. $service1,
  146. $service2,
  147. ];
  148. $newServices->defineService(
  149. 'Test',
  150. function () use ( &$instantiatorReturnValues ) {
  151. return array_shift( $instantiatorReturnValues );
  152. }
  153. );
  154. // force the service to become active, so we can check that it does get destroyed
  155. $oldTestService = $newServices->getService( 'Test' );
  156. MediaWikiServices::resetChildProcessServices();
  157. $finalServices = MediaWikiServices::getInstance();
  158. $newTestService = $finalServices->getService( 'Test' );
  159. $this->assertNotSame( $oldTestService, $newTestService );
  160. MediaWikiServices::forceGlobalInstance( $oldServices );
  161. }
  162. public function testResetServiceForTesting() {
  163. $services = $this->newMediaWikiServices();
  164. $serviceCounter = 0;
  165. $services->defineService(
  166. 'Test',
  167. function () use ( &$serviceCounter ) {
  168. $serviceCounter++;
  169. $service = $this->createMock( MediaWiki\Services\DestructibleService::class );
  170. $service->expects( $this->once() )->method( 'destroy' );
  171. return $service;
  172. }
  173. );
  174. // This should do nothing. In particular, it should not create a service instance.
  175. $services->resetServiceForTesting( 'Test' );
  176. $this->assertEquals( 0, $serviceCounter, 'No service instance should be created yet.' );
  177. $oldInstance = $services->getService( 'Test' );
  178. $this->assertEquals( 1, $serviceCounter, 'A service instance should exit now.' );
  179. // The old instance should be detached, and destroy() called.
  180. $services->resetServiceForTesting( 'Test' );
  181. $newInstance = $services->getService( 'Test' );
  182. $this->assertNotSame( $oldInstance, $newInstance );
  183. // Satisfy the expectation that destroy() is called also for the second service instance.
  184. $newInstance->destroy();
  185. }
  186. public function testResetServiceForTesting_noDestroy() {
  187. $services = $this->newMediaWikiServices();
  188. $services->defineService(
  189. 'Test',
  190. function () {
  191. $service = $this->createMock( MediaWiki\Services\DestructibleService::class );
  192. $service->expects( $this->never() )->method( 'destroy' );
  193. return $service;
  194. }
  195. );
  196. $oldInstance = $services->getService( 'Test' );
  197. // The old instance should be detached, but destroy() not called.
  198. $services->resetServiceForTesting( 'Test', false );
  199. $newInstance = $services->getService( 'Test' );
  200. $this->assertNotSame( $oldInstance, $newInstance );
  201. }
  202. public function provideGetters() {
  203. $getServiceCases = $this->provideGetService();
  204. $getterCases = [];
  205. // All getters should be named just like the service, with "get" added.
  206. foreach ( $getServiceCases as $name => $case ) {
  207. if ( $name[0] === '_' ) {
  208. // Internal service, no getter
  209. continue;
  210. }
  211. list( $service, $class ) = $case;
  212. $getterCases[$name] = [
  213. 'get' . $service,
  214. $class,
  215. ];
  216. }
  217. return $getterCases;
  218. }
  219. /**
  220. * @dataProvider provideGetters
  221. */
  222. public function testGetters( $getter, $type ) {
  223. // Test against the default instance, since the dummy will not know the default services.
  224. $services = MediaWikiServices::getInstance();
  225. $service = $services->$getter();
  226. $this->assertInstanceOf( $type, $service );
  227. }
  228. public function provideGetService() {
  229. global $IP;
  230. $serviceList = require "$IP/includes/ServiceWiring.php";
  231. $ret = [];
  232. foreach ( $serviceList as $name => $callback ) {
  233. $fun = new ReflectionFunction( $callback );
  234. if ( !$fun->hasReturnType() ) {
  235. throw new MWException( 'All service callbacks must have a return type defined, ' .
  236. "none found for $name" );
  237. }
  238. $ret[$name] = [ $name, $fun->getReturnType()->__toString() ];
  239. }
  240. return $ret;
  241. }
  242. /**
  243. * @dataProvider provideGetService
  244. */
  245. public function testGetService( $name, $type ) {
  246. // Test against the default instance, since the dummy will not know the default services.
  247. $services = MediaWikiServices::getInstance();
  248. $service = $services->getService( $name );
  249. $this->assertInstanceOf( $type, $service );
  250. }
  251. public function testDefaultServiceInstantiation() {
  252. // Check all services in the default instance, not a dummy instance!
  253. // Note that we instantiate all services here, including any that
  254. // were registered by extensions.
  255. $services = MediaWikiServices::getInstance();
  256. $names = $services->getServiceNames();
  257. foreach ( $names as $name ) {
  258. $this->assertTrue( $services->hasService( $name ) );
  259. $service = $services->getService( $name );
  260. $this->assertInternalType( 'object', $service );
  261. }
  262. }
  263. public function testDefaultServiceWiringServicesHaveTests() {
  264. global $IP;
  265. $testedServices = array_keys( $this->provideGetService() );
  266. $allServices = array_keys( require "$IP/includes/ServiceWiring.php" );
  267. $this->assertEquals(
  268. [],
  269. array_diff( $allServices, $testedServices ),
  270. 'The following services have not been added to MediaWikiServicesTest::provideGetService'
  271. );
  272. }
  273. public function testGettersAreSorted() {
  274. $methods = ( new ReflectionClass( MediaWikiServices::class ) )
  275. ->getMethods( ReflectionMethod::IS_STATIC | ReflectionMethod::IS_PUBLIC );
  276. $names = array_map( function ( $method ) {
  277. return $method->getName();
  278. }, $methods );
  279. $serviceNames = array_map( function ( $name ) {
  280. return "get$name";
  281. }, array_keys( $this->provideGetService() ) );
  282. $names = array_values( array_filter( $names, function ( $name ) use ( $serviceNames ) {
  283. return in_array( $name, $serviceNames );
  284. } ) );
  285. $sortedNames = $names;
  286. sort( $sortedNames );
  287. $this->assertSame( $sortedNames, $names,
  288. 'Please keep service getters sorted alphabetically' );
  289. }
  290. }