Merge "deferred: make DeferredUpdates::attemptUpdate() use callback owners for $fname...
[lhc/web/wiklou.git] / tests / phpunit / unit / includes / libs / services / ServiceContainerTest.php
1 <?php
2
3 use Wikimedia\Services\ServiceContainer;
4
5 /**
6 * @covers Wikimedia\Services\ServiceContainer
7 */
8 class ServiceContainerTest extends PHPUnit\Framework\TestCase {
9
10 use MediaWikiCoversValidator; // TODO this library is supposed to be independent of MediaWiki
11 use PHPUnit4And6Compat;
12
13 private function newServiceContainer( $extraArgs = [] ) {
14 return new ServiceContainer( $extraArgs );
15 }
16
17 public function testGetServiceNames() {
18 $services = $this->newServiceContainer();
19 $names = $services->getServiceNames();
20
21 $this->assertInternalType( 'array', $names );
22 $this->assertEmpty( $names );
23
24 $name = 'TestService92834576';
25 $services->defineService( $name, function () {
26 return null;
27 } );
28
29 $names = $services->getServiceNames();
30 $this->assertContains( $name, $names );
31 }
32
33 public function testHasService() {
34 $services = $this->newServiceContainer();
35
36 $name = 'TestService92834576';
37 $this->assertFalse( $services->hasService( $name ) );
38
39 $services->defineService( $name, function () {
40 return null;
41 } );
42
43 $this->assertTrue( $services->hasService( $name ) );
44 }
45
46 public function testGetService() {
47 $services = $this->newServiceContainer( [ 'Foo' ] );
48
49 $theService = new stdClass();
50 $name = 'TestService92834576';
51 $count = 0;
52
53 $services->defineService(
54 $name,
55 function ( $actualLocator, $extra ) use ( $services, $theService, &$count ) {
56 $count++;
57 PHPUnit_Framework_Assert::assertSame( $services, $actualLocator );
58 PHPUnit_Framework_Assert::assertSame( $extra, 'Foo' );
59 return $theService;
60 }
61 );
62
63 $this->assertSame( $theService, $services->getService( $name ) );
64
65 $services->getService( $name );
66 $this->assertSame( 1, $count, 'instantiator should be called exactly once!' );
67 }
68
69 public function testGetServiceRecursionCheck() {
70 $services = $this->newServiceContainer();
71
72 $services->defineService( 'service1', function ( ServiceContainer $services ) {
73 $services->getService( 'service2' );
74 } );
75
76 $services->defineService( 'service2', function ( ServiceContainer $services ) {
77 $services->getService( 'service3' );
78 } );
79
80 $services->defineService( 'service3', function ( ServiceContainer $services ) {
81 $services->getService( 'service1' );
82 } );
83
84 $exceptionThrown = false;
85 try {
86 $services->getService( 'service1' );
87 } catch ( RuntimeException $e ) {
88 $exceptionThrown = true;
89 $this->assertSame( 'Circular dependency when creating service! ' .
90 'service1 -> service2 -> service3 -> service1', $e->getMessage() );
91 }
92 $this->assertTrue( $exceptionThrown, 'RuntimeException must be thrown' );
93 }
94
95 public function testGetService_fail_unknown() {
96 $services = $this->newServiceContainer();
97
98 $name = 'TestService92834576';
99
100 $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class );
101
102 $services->getService( $name );
103 }
104
105 public function testPeekService() {
106 $services = $this->newServiceContainer();
107
108 $services->defineService(
109 'Foo',
110 function () {
111 return new stdClass();
112 }
113 );
114
115 $services->defineService(
116 'Bar',
117 function () {
118 return new stdClass();
119 }
120 );
121
122 // trigger instantiation of Foo
123 $services->getService( 'Foo' );
124
125 $this->assertInternalType(
126 'object',
127 $services->peekService( 'Foo' ),
128 'Peek should return the service object if it had been accessed before.'
129 );
130
131 $this->assertNull(
132 $services->peekService( 'Bar' ),
133 'Peek should return null if the service was never accessed.'
134 );
135 }
136
137 public function testPeekService_fail_unknown() {
138 $services = $this->newServiceContainer();
139
140 $name = 'TestService92834576';
141
142 $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class );
143
144 $services->peekService( $name );
145 }
146
147 public function testDefineService() {
148 $services = $this->newServiceContainer();
149
150 $theService = new stdClass();
151 $name = 'TestService92834576';
152
153 $services->defineService( $name, function ( $actualLocator ) use ( $services, $theService ) {
154 PHPUnit_Framework_Assert::assertSame( $services, $actualLocator );
155 return $theService;
156 } );
157
158 $this->assertTrue( $services->hasService( $name ) );
159 $this->assertSame( $theService, $services->getService( $name ) );
160 }
161
162 public function testDefineService_fail_duplicate() {
163 $services = $this->newServiceContainer();
164
165 $theService = new stdClass();
166 $name = 'TestService92834576';
167
168 $services->defineService( $name, function () use ( $theService ) {
169 return $theService;
170 } );
171
172 $this->setExpectedException( Wikimedia\Services\ServiceAlreadyDefinedException::class );
173
174 $services->defineService( $name, function () use ( $theService ) {
175 return $theService;
176 } );
177 }
178
179 public function testApplyWiring() {
180 $services = $this->newServiceContainer();
181
182 $wiring = [
183 'Foo' => function () {
184 return 'Foo!';
185 },
186 'Bar' => function () {
187 return 'Bar!';
188 },
189 ];
190
191 $services->applyWiring( $wiring );
192
193 $this->assertSame( 'Foo!', $services->getService( 'Foo' ) );
194 $this->assertSame( 'Bar!', $services->getService( 'Bar' ) );
195 }
196
197 public function testImportWiring() {
198 $services = $this->newServiceContainer();
199
200 $wiring = [
201 'Foo' => function () {
202 return 'Foo!';
203 },
204 'Bar' => function () {
205 return 'Bar!';
206 },
207 'Car' => function () {
208 return 'FUBAR!';
209 },
210 ];
211
212 $services->applyWiring( $wiring );
213
214 $services->addServiceManipulator( 'Foo', function ( $service ) {
215 return $service . '+X';
216 } );
217
218 $services->addServiceManipulator( 'Car', function ( $service ) {
219 return $service . '+X';
220 } );
221
222 $newServices = $this->newServiceContainer();
223
224 // create a service with manipulator
225 $newServices->defineService( 'Foo', function () {
226 return 'Foo!';
227 } );
228
229 $newServices->addServiceManipulator( 'Foo', function ( $service ) {
230 return $service . '+Y';
231 } );
232
233 // create a service before importing, so we can later check that
234 // existing service instances survive importWiring()
235 $newServices->defineService( 'Car', function () {
236 return 'Car!';
237 } );
238
239 // force instantiation
240 $newServices->getService( 'Car' );
241
242 // Define another service, so we can later check that extra wiring
243 // is not lost.
244 $newServices->defineService( 'Xar', function () {
245 return 'Xar!';
246 } );
247
248 // import wiring, but skip `Bar`
249 $newServices->importWiring( $services, [ 'Bar' ] );
250
251 $this->assertNotContains( 'Bar', $newServices->getServiceNames(), 'Skip `Bar` service' );
252 $this->assertSame( 'Foo!+Y+X', $newServices->getService( 'Foo' ) );
253
254 // import all wiring, but preserve existing service instance
255 $newServices->importWiring( $services );
256
257 $this->assertContains( 'Bar', $newServices->getServiceNames(), 'Import all services' );
258 $this->assertSame( 'Bar!', $newServices->getService( 'Bar' ) );
259 $this->assertSame( 'Car!', $newServices->getService( 'Car' ), 'Use existing service instance' );
260 $this->assertSame( 'Xar!', $newServices->getService( 'Xar' ), 'Predefined services are kept' );
261 }
262
263 public function testLoadWiringFiles() {
264 $services = $this->newServiceContainer();
265
266 $wiringFiles = [
267 __DIR__ . '/TestWiring1.php',
268 __DIR__ . '/TestWiring2.php',
269 ];
270
271 $services->loadWiringFiles( $wiringFiles );
272
273 $this->assertSame( 'Foo!', $services->getService( 'Foo' ) );
274 $this->assertSame( 'Bar!', $services->getService( 'Bar' ) );
275 }
276
277 public function testLoadWiringFiles_fail_duplicate() {
278 $services = $this->newServiceContainer();
279
280 $wiringFiles = [
281 __DIR__ . '/TestWiring1.php',
282 __DIR__ . '/./TestWiring1.php',
283 ];
284
285 // loading the same file twice should fail, because
286 $this->setExpectedException( Wikimedia\Services\ServiceAlreadyDefinedException::class );
287
288 $services->loadWiringFiles( $wiringFiles );
289 }
290
291 public function testRedefineService() {
292 $services = $this->newServiceContainer( [ 'Foo' ] );
293
294 $theService1 = new stdClass();
295 $name = 'TestService92834576';
296
297 $services->defineService( $name, function () {
298 PHPUnit_Framework_Assert::fail(
299 'The original instantiator function should not get called'
300 );
301 } );
302
303 // redefine before instantiation
304 $services->redefineService(
305 $name,
306 function ( $actualLocator, $extra ) use ( $services, $theService1 ) {
307 PHPUnit_Framework_Assert::assertSame( $services, $actualLocator );
308 PHPUnit_Framework_Assert::assertSame( 'Foo', $extra );
309 return $theService1;
310 }
311 );
312
313 // force instantiation, check result
314 $this->assertSame( $theService1, $services->getService( $name ) );
315 }
316
317 public function testRedefineService_disabled() {
318 $services = $this->newServiceContainer( [ 'Foo' ] );
319
320 $theService1 = new stdClass();
321 $name = 'TestService92834576';
322
323 $services->defineService( $name, function () {
324 return 'Foo';
325 } );
326
327 // disable the service. we should be able to redefine it anyway.
328 $services->disableService( $name );
329
330 $services->redefineService( $name, function () use ( $theService1 ) {
331 return $theService1;
332 } );
333
334 // force instantiation, check result
335 $this->assertSame( $theService1, $services->getService( $name ) );
336 }
337
338 public function testRedefineService_fail_undefined() {
339 $services = $this->newServiceContainer();
340
341 $theService = new stdClass();
342 $name = 'TestService92834576';
343
344 $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class );
345
346 $services->redefineService( $name, function () use ( $theService ) {
347 return $theService;
348 } );
349 }
350
351 public function testRedefineService_fail_in_use() {
352 $services = $this->newServiceContainer( [ 'Foo' ] );
353
354 $theService = new stdClass();
355 $name = 'TestService92834576';
356
357 $services->defineService( $name, function () {
358 return 'Foo';
359 } );
360
361 // create the service, so it can no longer be redefined
362 $services->getService( $name );
363
364 $this->setExpectedException( Wikimedia\Services\CannotReplaceActiveServiceException::class );
365
366 $services->redefineService( $name, function () use ( $theService ) {
367 return $theService;
368 } );
369 }
370
371 public function testAddServiceManipulator() {
372 $services = $this->newServiceContainer( [ 'Foo' ] );
373
374 $theService1 = new stdClass();
375 $theService2 = new stdClass();
376 $name = 'TestService92834576';
377
378 $services->defineService(
379 $name,
380 function ( $actualLocator, $extra ) use ( $services, $theService1 ) {
381 PHPUnit_Framework_Assert::assertSame( $services, $actualLocator );
382 PHPUnit_Framework_Assert::assertSame( 'Foo', $extra );
383 return $theService1;
384 }
385 );
386
387 $services->addServiceManipulator(
388 $name,
389 function (
390 $theService, $actualLocator, $extra
391 ) use (
392 $services, $theService1, $theService2
393 ) {
394 PHPUnit_Framework_Assert::assertSame( $theService1, $theService );
395 PHPUnit_Framework_Assert::assertSame( $services, $actualLocator );
396 PHPUnit_Framework_Assert::assertSame( 'Foo', $extra );
397 return $theService2;
398 }
399 );
400
401 // force instantiation, check result
402 $this->assertSame( $theService2, $services->getService( $name ) );
403 }
404
405 public function testAddServiceManipulator_fail_undefined() {
406 $services = $this->newServiceContainer();
407
408 $theService = new stdClass();
409 $name = 'TestService92834576';
410
411 $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class );
412
413 $services->addServiceManipulator( $name, function () use ( $theService ) {
414 return $theService;
415 } );
416 }
417
418 public function testAddServiceManipulator_fail_in_use() {
419 $services = $this->newServiceContainer( [ 'Foo' ] );
420
421 $theService = new stdClass();
422 $name = 'TestService92834576';
423
424 $services->defineService( $name, function () use ( $theService ) {
425 return $theService;
426 } );
427
428 // create the service, so it can no longer be redefined
429 $services->getService( $name );
430
431 $this->setExpectedException( Wikimedia\Services\CannotReplaceActiveServiceException::class );
432
433 $services->addServiceManipulator( $name, function () {
434 return 'Foo';
435 } );
436 }
437
438 public function testDisableService() {
439 $services = $this->newServiceContainer( [ 'Foo' ] );
440
441 $destructible = $this->getMockBuilder( Wikimedia\Services\DestructibleService::class )
442 ->getMock();
443 $destructible->expects( $this->once() )
444 ->method( 'destroy' );
445
446 $services->defineService( 'Foo', function () use ( $destructible ) {
447 return $destructible;
448 } );
449 $services->defineService( 'Bar', function () {
450 return new stdClass();
451 } );
452 $services->defineService( 'Qux', function () {
453 return new stdClass();
454 } );
455
456 // instantiate Foo and Bar services
457 $services->getService( 'Foo' );
458 $services->getService( 'Bar' );
459
460 // disable service, should call destroy() once.
461 $services->disableService( 'Foo' );
462
463 // disabled service should still be listed
464 $this->assertContains( 'Foo', $services->getServiceNames() );
465
466 // getting other services should still work
467 $services->getService( 'Bar' );
468
469 // disable non-destructible service, and not-yet-instantiated service
470 $services->disableService( 'Bar' );
471 $services->disableService( 'Qux' );
472
473 $this->assertNull( $services->peekService( 'Bar' ) );
474 $this->assertNull( $services->peekService( 'Qux' ) );
475
476 // disabled service should still be listed
477 $this->assertContains( 'Bar', $services->getServiceNames() );
478 $this->assertContains( 'Qux', $services->getServiceNames() );
479
480 $this->setExpectedException( Wikimedia\Services\ServiceDisabledException::class );
481 $services->getService( 'Qux' );
482 }
483
484 public function testDisableService_fail_undefined() {
485 $services = $this->newServiceContainer();
486
487 $theService = new stdClass();
488 $name = 'TestService92834576';
489
490 $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class );
491
492 $services->redefineService( $name, function () use ( $theService ) {
493 return $theService;
494 } );
495 }
496
497 public function testDestroy() {
498 $services = $this->newServiceContainer();
499
500 $destructible = $this->getMockBuilder( Wikimedia\Services\DestructibleService::class )
501 ->getMock();
502 $destructible->expects( $this->once() )
503 ->method( 'destroy' );
504
505 $services->defineService( 'Foo', function () use ( $destructible ) {
506 return $destructible;
507 } );
508
509 $services->defineService( 'Bar', function () {
510 return new stdClass();
511 } );
512
513 // create the service
514 $services->getService( 'Foo' );
515
516 // destroy the container
517 $services->destroy();
518
519 $this->setExpectedException( Wikimedia\Services\ContainerDisabledException::class );
520 $services->getService( 'Bar' );
521 }
522
523 }