Merge "Reuse different message in SpecialEditWatchlist"
[lhc/web/wiklou.git] / tests / phpunit / includes / MediaWikiServicesTest.php
1 <?php
2
3 use Mediawiki\Http\HttpRequestFactory;
4 use MediaWiki\Interwiki\InterwikiLookup;
5 use MediaWiki\Linker\LinkRenderer;
6 use MediaWiki\Linker\LinkRendererFactory;
7 use MediaWiki\MediaWikiServices;
8 use MediaWiki\Preferences\PreferencesFactory;
9 use MediaWiki\Services\DestructibleService;
10 use MediaWiki\Services\SalvageableService;
11 use MediaWiki\Services\ServiceDisabledException;
12 use MediaWiki\Shell\CommandFactory;
13 use MediaWiki\Storage\BlobStore;
14 use MediaWiki\Storage\BlobStoreFactory;
15 use MediaWiki\Storage\NameTableStore;
16 use MediaWiki\Storage\RevisionFactory;
17 use MediaWiki\Storage\RevisionLookup;
18 use MediaWiki\Storage\RevisionStore;
19 use MediaWiki\Storage\RevisionStoreFactory;
20 use MediaWiki\Storage\SqlBlobStore;
21
22 /**
23 * @covers MediaWiki\MediaWikiServices
24 *
25 * @group MediaWiki
26 */
27 class MediaWikiServicesTest extends MediaWikiTestCase {
28
29 /**
30 * @return Config
31 */
32 private function newTestConfig() {
33 $globalConfig = new GlobalVarConfig();
34
35 $testConfig = new HashConfig();
36 $testConfig->set( 'ServiceWiringFiles', $globalConfig->get( 'ServiceWiringFiles' ) );
37 $testConfig->set( 'ConfigRegistry', $globalConfig->get( 'ConfigRegistry' ) );
38
39 return $testConfig;
40 }
41
42 /**
43 * @return MediaWikiServices
44 */
45 private function newMediaWikiServices( Config $config = null ) {
46 if ( $config === null ) {
47 $config = $this->newTestConfig();
48 }
49
50 $instance = new MediaWikiServices( $config );
51
52 // Load the default wiring from the specified files.
53 $wiringFiles = $config->get( 'ServiceWiringFiles' );
54 $instance->loadWiringFiles( $wiringFiles );
55
56 return $instance;
57 }
58
59 public function testGetInstance() {
60 $services = MediaWikiServices::getInstance();
61 $this->assertInstanceOf( MediaWikiServices::class, $services );
62 }
63
64 public function testForceGlobalInstance() {
65 $newServices = $this->newMediaWikiServices();
66 $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
67
68 $this->assertInstanceOf( MediaWikiServices::class, $oldServices );
69 $this->assertNotSame( $oldServices, $newServices );
70
71 $theServices = MediaWikiServices::getInstance();
72 $this->assertSame( $theServices, $newServices );
73
74 MediaWikiServices::forceGlobalInstance( $oldServices );
75
76 $theServices = MediaWikiServices::getInstance();
77 $this->assertSame( $theServices, $oldServices );
78 }
79
80 public function testResetGlobalInstance() {
81 $newServices = $this->newMediaWikiServices();
82 $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
83
84 $service1 = $this->createMock( SalvageableService::class );
85 $service1->expects( $this->never() )
86 ->method( 'salvage' );
87
88 $newServices->defineService(
89 'Test',
90 function () use ( $service1 ) {
91 return $service1;
92 }
93 );
94
95 // force instantiation
96 $newServices->getService( 'Test' );
97
98 MediaWikiServices::resetGlobalInstance( $this->newTestConfig() );
99 $theServices = MediaWikiServices::getInstance();
100
101 $this->assertSame(
102 $service1,
103 $theServices->getService( 'Test' ),
104 'service definition should survive reset'
105 );
106
107 $this->assertNotSame( $theServices, $newServices );
108 $this->assertNotSame( $theServices, $oldServices );
109
110 MediaWikiServices::forceGlobalInstance( $oldServices );
111 }
112
113 public function testResetGlobalInstance_quick() {
114 $newServices = $this->newMediaWikiServices();
115 $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
116
117 $service1 = $this->createMock( SalvageableService::class );
118 $service1->expects( $this->never() )
119 ->method( 'salvage' );
120
121 $service2 = $this->createMock( SalvageableService::class );
122 $service2->expects( $this->once() )
123 ->method( 'salvage' )
124 ->with( $service1 );
125
126 // sequence of values the instantiator will return
127 $instantiatorReturnValues = [
128 $service1,
129 $service2,
130 ];
131
132 $newServices->defineService(
133 'Test',
134 function () use ( &$instantiatorReturnValues ) {
135 return array_shift( $instantiatorReturnValues );
136 }
137 );
138
139 // force instantiation
140 $newServices->getService( 'Test' );
141
142 MediaWikiServices::resetGlobalInstance( $this->newTestConfig(), 'quick' );
143 $theServices = MediaWikiServices::getInstance();
144
145 $this->assertSame( $service2, $theServices->getService( 'Test' ) );
146
147 $this->assertNotSame( $theServices, $newServices );
148 $this->assertNotSame( $theServices, $oldServices );
149
150 MediaWikiServices::forceGlobalInstance( $oldServices );
151 }
152
153 public function testDisableStorageBackend() {
154 $newServices = $this->newMediaWikiServices();
155 $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
156
157 $lbFactory = $this->getMockBuilder( \Wikimedia\Rdbms\LBFactorySimple::class )
158 ->disableOriginalConstructor()
159 ->getMock();
160
161 $newServices->redefineService(
162 'DBLoadBalancerFactory',
163 function () use ( $lbFactory ) {
164 return $lbFactory;
165 }
166 );
167
168 // force the service to become active, so we can check that it does get destroyed
169 $newServices->getService( 'DBLoadBalancerFactory' );
170
171 MediaWikiServices::disableStorageBackend(); // should destroy DBLoadBalancerFactory
172
173 try {
174 MediaWikiServices::getInstance()->getService( 'DBLoadBalancerFactory' );
175 $this->fail( 'DBLoadBalancerFactory should have been disabled' );
176 }
177 catch ( ServiceDisabledException $ex ) {
178 // ok, as expected
179 } catch ( Throwable $ex ) {
180 $this->fail( 'ServiceDisabledException expected, caught ' . get_class( $ex ) );
181 }
182
183 MediaWikiServices::forceGlobalInstance( $oldServices );
184 $newServices->destroy();
185
186 // No exception was thrown, avoid being risky
187 $this->assertTrue( true );
188 }
189
190 public function testResetChildProcessServices() {
191 $newServices = $this->newMediaWikiServices();
192 $oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
193
194 $service1 = $this->createMock( DestructibleService::class );
195 $service1->expects( $this->once() )
196 ->method( 'destroy' );
197
198 $service2 = $this->createMock( DestructibleService::class );
199 $service2->expects( $this->never() )
200 ->method( 'destroy' );
201
202 // sequence of values the instantiator will return
203 $instantiatorReturnValues = [
204 $service1,
205 $service2,
206 ];
207
208 $newServices->defineService(
209 'Test',
210 function () use ( &$instantiatorReturnValues ) {
211 return array_shift( $instantiatorReturnValues );
212 }
213 );
214
215 // force the service to become active, so we can check that it does get destroyed
216 $oldTestService = $newServices->getService( 'Test' );
217
218 MediaWikiServices::resetChildProcessServices();
219 $finalServices = MediaWikiServices::getInstance();
220
221 $newTestService = $finalServices->getService( 'Test' );
222 $this->assertNotSame( $oldTestService, $newTestService );
223
224 MediaWikiServices::forceGlobalInstance( $oldServices );
225 }
226
227 public function testResetServiceForTesting() {
228 $services = $this->newMediaWikiServices();
229 $serviceCounter = 0;
230
231 $services->defineService(
232 'Test',
233 function () use ( &$serviceCounter ) {
234 $serviceCounter++;
235 $service = $this->createMock( MediaWiki\Services\DestructibleService::class );
236 $service->expects( $this->once() )->method( 'destroy' );
237 return $service;
238 }
239 );
240
241 // This should do nothing. In particular, it should not create a service instance.
242 $services->resetServiceForTesting( 'Test' );
243 $this->assertEquals( 0, $serviceCounter, 'No service instance should be created yet.' );
244
245 $oldInstance = $services->getService( 'Test' );
246 $this->assertEquals( 1, $serviceCounter, 'A service instance should exit now.' );
247
248 // The old instance should be detached, and destroy() called.
249 $services->resetServiceForTesting( 'Test' );
250 $newInstance = $services->getService( 'Test' );
251
252 $this->assertNotSame( $oldInstance, $newInstance );
253
254 // Satisfy the expectation that destroy() is called also for the second service instance.
255 $newInstance->destroy();
256 }
257
258 public function testResetServiceForTesting_noDestroy() {
259 $services = $this->newMediaWikiServices();
260
261 $services->defineService(
262 'Test',
263 function () {
264 $service = $this->createMock( MediaWiki\Services\DestructibleService::class );
265 $service->expects( $this->never() )->method( 'destroy' );
266 return $service;
267 }
268 );
269
270 $oldInstance = $services->getService( 'Test' );
271
272 // The old instance should be detached, but destroy() not called.
273 $services->resetServiceForTesting( 'Test', false );
274 $newInstance = $services->getService( 'Test' );
275
276 $this->assertNotSame( $oldInstance, $newInstance );
277 }
278
279 public function provideGetters() {
280 $getServiceCases = $this->provideGetService();
281 $getterCases = [];
282
283 // All getters should be named just like the service, with "get" added.
284 foreach ( $getServiceCases as $name => $case ) {
285 if ( $name[0] === '_' ) {
286 // Internal service, no getter
287 continue;
288 }
289 list( $service, $class ) = $case;
290 $getterCases[$name] = [
291 'get' . $service,
292 $class,
293 ];
294 }
295
296 return $getterCases;
297 }
298
299 /**
300 * @dataProvider provideGetters
301 */
302 public function testGetters( $getter, $type ) {
303 // Test against the default instance, since the dummy will not know the default services.
304 $services = MediaWikiServices::getInstance();
305 $service = $services->$getter();
306 $this->assertInstanceOf( $type, $service );
307 }
308
309 public function provideGetService() {
310 // NOTE: This should list all service getters defined in ServiceWiring.php.
311 return [
312 'BootstrapConfig' => [ 'BootstrapConfig', Config::class ],
313 'ConfigFactory' => [ 'ConfigFactory', ConfigFactory::class ],
314 'MainConfig' => [ 'MainConfig', Config::class ],
315 'SiteStore' => [ 'SiteStore', SiteStore::class ],
316 'SiteLookup' => [ 'SiteLookup', SiteLookup::class ],
317 'StatsdDataFactory' => [ 'StatsdDataFactory', IBufferingStatsdDataFactory::class ],
318 'PerDbNameStatsdDataFactory' =>
319 [ 'PerDbNameStatsdDataFactory', IBufferingStatsdDataFactory::class ],
320 'InterwikiLookup' => [ 'InterwikiLookup', InterwikiLookup::class ],
321 'EventRelayerGroup' => [ 'EventRelayerGroup', EventRelayerGroup::class ],
322 'SearchEngineFactory' => [ 'SearchEngineFactory', SearchEngineFactory::class ],
323 'SearchEngineConfig' => [ 'SearchEngineConfig', SearchEngineConfig::class ],
324 'SkinFactory' => [ 'SkinFactory', SkinFactory::class ],
325 'DBLoadBalancerFactory' => [ 'DBLoadBalancerFactory', Wikimedia\Rdbms\LBFactory::class ],
326 'DBLoadBalancer' => [ 'DBLoadBalancer', Wikimedia\Rdbms\LoadBalancer::class ],
327 'WatchedItemStore' => [ 'WatchedItemStore', WatchedItemStore::class ],
328 'WatchedItemQueryService' => [ 'WatchedItemQueryService', WatchedItemQueryService::class ],
329 'CryptRand' => [ 'CryptRand', CryptRand::class ],
330 'CryptHKDF' => [ 'CryptHKDF', CryptHKDF::class ],
331 'MediaHandlerFactory' => [ 'MediaHandlerFactory', MediaHandlerFactory::class ],
332 'Parser' => [ 'Parser', Parser::class ],
333 'ParserCache' => [ 'ParserCache', ParserCache::class ],
334 'GenderCache' => [ 'GenderCache', GenderCache::class ],
335 'LinkCache' => [ 'LinkCache', LinkCache::class ],
336 'LinkRenderer' => [ 'LinkRenderer', LinkRenderer::class ],
337 'LinkRendererFactory' => [ 'LinkRendererFactory', LinkRendererFactory::class ],
338 '_MediaWikiTitleCodec' => [ '_MediaWikiTitleCodec', MediaWikiTitleCodec::class ],
339 'MimeAnalyzer' => [ 'MimeAnalyzer', MimeAnalyzer::class ],
340 'TitleFormatter' => [ 'TitleFormatter', TitleFormatter::class ],
341 'TitleParser' => [ 'TitleParser', TitleParser::class ],
342 'ProxyLookup' => [ 'ProxyLookup', ProxyLookup::class ],
343 'MainObjectStash' => [ 'MainObjectStash', BagOStuff::class ],
344 'MainWANObjectCache' => [ 'MainWANObjectCache', WANObjectCache::class ],
345 'LocalServerObjectCache' => [ 'LocalServerObjectCache', BagOStuff::class ],
346 'VirtualRESTServiceClient' => [ 'VirtualRESTServiceClient', VirtualRESTServiceClient::class ],
347 'ShellCommandFactory' => [ 'ShellCommandFactory', CommandFactory::class ],
348 'BlobStoreFactory' => [ 'BlobStoreFactory', BlobStoreFactory::class ],
349 'BlobStore' => [ 'BlobStore', BlobStore::class ],
350 '_SqlBlobStore' => [ '_SqlBlobStore', SqlBlobStore::class ],
351 'RevisionStore' => [ 'RevisionStore', RevisionStore::class ],
352 'RevisionStoreFactory' => [ 'RevisionStoreFactory', RevisionStoreFactory::class ],
353 'RevisionLookup' => [ 'RevisionLookup', RevisionLookup::class ],
354 'RevisionFactory' => [ 'RevisionFactory', RevisionFactory::class ],
355 'ContentModelStore' => [ 'ContentModelStore', NameTableStore::class ],
356 'SlotRoleStore' => [ 'SlotRoleStore', NameTableStore::class ],
357 'HttpRequestFactory' => [ 'HttpRequestFactory', HttpRequestFactory::class ],
358 'CommentStore' => [ 'CommentStore', CommentStore::class ],
359 'ChangeTagDefStore' => [ 'ChangeTagDefStore', NameTableStore::class ],
360 'ConfiguredReadOnlyMode' => [ 'ConfiguredReadOnlyMode', ConfiguredReadOnlyMode::class ],
361 'ReadOnlyMode' => [ 'ReadOnlyMode', ReadOnlyMode::class ],
362 'UploadRevisionImporter' => [ 'UploadRevisionImporter', UploadRevisionImporter::class ],
363 'OldRevisionImporter' => [ 'OldRevisionImporter', OldRevisionImporter::class ],
364 'WikiRevisionOldRevisionImporterNoUpdates' =>
365 [ 'WikiRevisionOldRevisionImporterNoUpdates', ImportableOldRevisionImporter::class ],
366 'ExternalStoreFactory' => [ 'ExternalStoreFactory', ExternalStoreFactory::class ],
367 'PreferencesFactory' => [ 'PreferencesFactory', PreferencesFactory::class ],
368 'ActorMigration' => [ 'ActorMigration', ActorMigration::class ],
369 'ConfigRepository' => [ 'ConfigRepository', \MediaWiki\Config\ConfigRepository::class ],
370 'MagicWordFactory' => [ 'MagicWordFactory', MagicWordFactory::class ],
371 'ContentLanguage' => [ 'ContentLanguage', Language::class ],
372 'PasswordFactory' => [ 'PasswordFactory', PasswordFactory::class ],
373 ];
374 }
375
376 /**
377 * @dataProvider provideGetService
378 */
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();
382
383 $service = $services->getService( $name );
384 $this->assertInstanceOf( $type, $service );
385 }
386
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();
393
394 foreach ( $names as $name ) {
395 $this->assertTrue( $services->hasService( $name ) );
396 $service = $services->getService( $name );
397 $this->assertInternalType( 'object', $service );
398 }
399 }
400
401 public function testDefaultServiceWiringServicesHaveTests() {
402 global $IP;
403 $testedServices = array_keys( $this->provideGetService() );
404 $allServices = array_keys( include $IP . '/includes/ServiceWiring.php' );
405 $this->assertEquals(
406 [],
407 array_diff( $allServices, $testedServices ),
408 'The following services have not been added to MediaWikiServicesTest::provideGetService'
409 );
410 }
411
412 }