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