From: Aryeh Gregor Date: Wed, 14 Aug 2019 11:46:47 +0000 (+0300) Subject: Add recursion check to createService() X-Git-Tag: 1.34.0-rc.0~680^2 X-Git-Url: http://git.cyclocoop.org/%7B%7B%20url_for%28%27admin_vote_add%27%29%20%7D%7D?a=commitdiff_plain;h=a995d9be1dfc5741713ccb265b2178bc2a760386;p=lhc%2Fweb%2Fwiklou.git Add recursion check to createService() This will throw when trying to create a service while already in the process of creating that same service, i.e., if there's a circular service dependency. This would have saved me a whole bunch of debugging time. :) Change-Id: Id148d4f221f35f4069f3e0ab0069d13ca271df3d --- diff --git a/includes/libs/services/ServiceContainer.php b/includes/libs/services/ServiceContainer.php index d1f10524d5..84755edb90 100644 --- a/includes/libs/services/ServiceContainer.php +++ b/includes/libs/services/ServiceContainer.php @@ -6,6 +6,7 @@ use InvalidArgumentException; use Psr\Container\ContainerInterface; use RuntimeException; use Wikimedia\Assert\Assert; +use Wikimedia\ScopedCallback; /** * Generic service container. @@ -77,6 +78,11 @@ class ServiceContainer implements ContainerInterface, DestructibleService { */ private $destroyed = false; + /** + * @var array Set of services currently being created, to detect loops + */ + private $servicesBeingCreated = []; + /** * @param array $extraInstantiationParams Any additional parameters to be passed to the * instantiator function when creating a service. This is typically used to provide @@ -433,10 +439,20 @@ class ServiceContainer implements ContainerInterface, DestructibleService { * @param string $name * * @throws InvalidArgumentException if $name is not a known service. + * @throws RuntimeException if a circular dependency is detected. * @return object */ private function createService( $name ) { if ( isset( $this->serviceInstantiators[$name] ) ) { + if ( isset( $this->servicesBeingCreated[$name] ) ) { + throw new RuntimeException( "Circular dependency when creating service! " . + implode( ' -> ', array_keys( $this->servicesBeingCreated ) ) . " -> $name" ); + } + $this->servicesBeingCreated[$name] = true; + $removeFromStack = new ScopedCallback( function () use ( $name ) { + unset( $this->servicesBeingCreated[$name] ); + } ); + $service = ( $this->serviceInstantiators[$name] )( $this, ...$this->extraInstantiationParams @@ -458,6 +474,8 @@ class ServiceContainer implements ContainerInterface, DestructibleService { } } + $removeFromStack->consume(); + // NOTE: when adding more wiring logic here, make sure importWiring() is kept in sync! } else { throw new NoSuchServiceException( $name ); diff --git a/tests/phpunit/includes/libs/services/ServiceContainerTest.php b/tests/phpunit/includes/libs/services/ServiceContainerTest.php deleted file mode 100644 index 6e51883cfb..0000000000 --- a/tests/phpunit/includes/libs/services/ServiceContainerTest.php +++ /dev/null @@ -1,497 +0,0 @@ -newServiceContainer(); - $names = $services->getServiceNames(); - - $this->assertInternalType( 'array', $names ); - $this->assertEmpty( $names ); - - $name = 'TestService92834576'; - $services->defineService( $name, function () { - return null; - } ); - - $names = $services->getServiceNames(); - $this->assertContains( $name, $names ); - } - - public function testHasService() { - $services = $this->newServiceContainer(); - - $name = 'TestService92834576'; - $this->assertFalse( $services->hasService( $name ) ); - - $services->defineService( $name, function () { - return null; - } ); - - $this->assertTrue( $services->hasService( $name ) ); - } - - public function testGetService() { - $services = $this->newServiceContainer( [ 'Foo' ] ); - - $theService = new stdClass(); - $name = 'TestService92834576'; - $count = 0; - - $services->defineService( - $name, - function ( $actualLocator, $extra ) use ( $services, $theService, &$count ) { - $count++; - PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); - PHPUnit_Framework_Assert::assertSame( $extra, 'Foo' ); - return $theService; - } - ); - - $this->assertSame( $theService, $services->getService( $name ) ); - - $services->getService( $name ); - $this->assertSame( 1, $count, 'instantiator should be called exactly once!' ); - } - - public function testGetService_fail_unknown() { - $services = $this->newServiceContainer(); - - $name = 'TestService92834576'; - - $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class ); - - $services->getService( $name ); - } - - public function testPeekService() { - $services = $this->newServiceContainer(); - - $services->defineService( - 'Foo', - function () { - return new stdClass(); - } - ); - - $services->defineService( - 'Bar', - function () { - return new stdClass(); - } - ); - - // trigger instantiation of Foo - $services->getService( 'Foo' ); - - $this->assertInternalType( - 'object', - $services->peekService( 'Foo' ), - 'Peek should return the service object if it had been accessed before.' - ); - - $this->assertNull( - $services->peekService( 'Bar' ), - 'Peek should return null if the service was never accessed.' - ); - } - - public function testPeekService_fail_unknown() { - $services = $this->newServiceContainer(); - - $name = 'TestService92834576'; - - $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class ); - - $services->peekService( $name ); - } - - public function testDefineService() { - $services = $this->newServiceContainer(); - - $theService = new stdClass(); - $name = 'TestService92834576'; - - $services->defineService( $name, function ( $actualLocator ) use ( $services, $theService ) { - PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); - return $theService; - } ); - - $this->assertTrue( $services->hasService( $name ) ); - $this->assertSame( $theService, $services->getService( $name ) ); - } - - public function testDefineService_fail_duplicate() { - $services = $this->newServiceContainer(); - - $theService = new stdClass(); - $name = 'TestService92834576'; - - $services->defineService( $name, function () use ( $theService ) { - return $theService; - } ); - - $this->setExpectedException( Wikimedia\Services\ServiceAlreadyDefinedException::class ); - - $services->defineService( $name, function () use ( $theService ) { - return $theService; - } ); - } - - public function testApplyWiring() { - $services = $this->newServiceContainer(); - - $wiring = [ - 'Foo' => function () { - return 'Foo!'; - }, - 'Bar' => function () { - return 'Bar!'; - }, - ]; - - $services->applyWiring( $wiring ); - - $this->assertSame( 'Foo!', $services->getService( 'Foo' ) ); - $this->assertSame( 'Bar!', $services->getService( 'Bar' ) ); - } - - public function testImportWiring() { - $services = $this->newServiceContainer(); - - $wiring = [ - 'Foo' => function () { - return 'Foo!'; - }, - 'Bar' => function () { - return 'Bar!'; - }, - 'Car' => function () { - return 'FUBAR!'; - }, - ]; - - $services->applyWiring( $wiring ); - - $services->addServiceManipulator( 'Foo', function ( $service ) { - return $service . '+X'; - } ); - - $services->addServiceManipulator( 'Car', function ( $service ) { - return $service . '+X'; - } ); - - $newServices = $this->newServiceContainer(); - - // create a service with manipulator - $newServices->defineService( 'Foo', function () { - return 'Foo!'; - } ); - - $newServices->addServiceManipulator( 'Foo', function ( $service ) { - return $service . '+Y'; - } ); - - // create a service before importing, so we can later check that - // existing service instances survive importWiring() - $newServices->defineService( 'Car', function () { - return 'Car!'; - } ); - - // force instantiation - $newServices->getService( 'Car' ); - - // Define another service, so we can later check that extra wiring - // is not lost. - $newServices->defineService( 'Xar', function () { - return 'Xar!'; - } ); - - // import wiring, but skip `Bar` - $newServices->importWiring( $services, [ 'Bar' ] ); - - $this->assertNotContains( 'Bar', $newServices->getServiceNames(), 'Skip `Bar` service' ); - $this->assertSame( 'Foo!+Y+X', $newServices->getService( 'Foo' ) ); - - // import all wiring, but preserve existing service instance - $newServices->importWiring( $services ); - - $this->assertContains( 'Bar', $newServices->getServiceNames(), 'Import all services' ); - $this->assertSame( 'Bar!', $newServices->getService( 'Bar' ) ); - $this->assertSame( 'Car!', $newServices->getService( 'Car' ), 'Use existing service instance' ); - $this->assertSame( 'Xar!', $newServices->getService( 'Xar' ), 'Predefined services are kept' ); - } - - public function testLoadWiringFiles() { - $services = $this->newServiceContainer(); - - $wiringFiles = [ - __DIR__ . '/TestWiring1.php', - __DIR__ . '/TestWiring2.php', - ]; - - $services->loadWiringFiles( $wiringFiles ); - - $this->assertSame( 'Foo!', $services->getService( 'Foo' ) ); - $this->assertSame( 'Bar!', $services->getService( 'Bar' ) ); - } - - public function testLoadWiringFiles_fail_duplicate() { - $services = $this->newServiceContainer(); - - $wiringFiles = [ - __DIR__ . '/TestWiring1.php', - __DIR__ . '/./TestWiring1.php', - ]; - - // loading the same file twice should fail, because - $this->setExpectedException( Wikimedia\Services\ServiceAlreadyDefinedException::class ); - - $services->loadWiringFiles( $wiringFiles ); - } - - public function testRedefineService() { - $services = $this->newServiceContainer( [ 'Foo' ] ); - - $theService1 = new stdClass(); - $name = 'TestService92834576'; - - $services->defineService( $name, function () { - PHPUnit_Framework_Assert::fail( - 'The original instantiator function should not get called' - ); - } ); - - // redefine before instantiation - $services->redefineService( - $name, - function ( $actualLocator, $extra ) use ( $services, $theService1 ) { - PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); - PHPUnit_Framework_Assert::assertSame( 'Foo', $extra ); - return $theService1; - } - ); - - // force instantiation, check result - $this->assertSame( $theService1, $services->getService( $name ) ); - } - - public function testRedefineService_disabled() { - $services = $this->newServiceContainer( [ 'Foo' ] ); - - $theService1 = new stdClass(); - $name = 'TestService92834576'; - - $services->defineService( $name, function () { - return 'Foo'; - } ); - - // disable the service. we should be able to redefine it anyway. - $services->disableService( $name ); - - $services->redefineService( $name, function () use ( $theService1 ) { - return $theService1; - } ); - - // force instantiation, check result - $this->assertSame( $theService1, $services->getService( $name ) ); - } - - public function testRedefineService_fail_undefined() { - $services = $this->newServiceContainer(); - - $theService = new stdClass(); - $name = 'TestService92834576'; - - $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class ); - - $services->redefineService( $name, function () use ( $theService ) { - return $theService; - } ); - } - - public function testRedefineService_fail_in_use() { - $services = $this->newServiceContainer( [ 'Foo' ] ); - - $theService = new stdClass(); - $name = 'TestService92834576'; - - $services->defineService( $name, function () { - return 'Foo'; - } ); - - // create the service, so it can no longer be redefined - $services->getService( $name ); - - $this->setExpectedException( Wikimedia\Services\CannotReplaceActiveServiceException::class ); - - $services->redefineService( $name, function () use ( $theService ) { - return $theService; - } ); - } - - public function testAddServiceManipulator() { - $services = $this->newServiceContainer( [ 'Foo' ] ); - - $theService1 = new stdClass(); - $theService2 = new stdClass(); - $name = 'TestService92834576'; - - $services->defineService( - $name, - function ( $actualLocator, $extra ) use ( $services, $theService1 ) { - PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); - PHPUnit_Framework_Assert::assertSame( 'Foo', $extra ); - return $theService1; - } - ); - - $services->addServiceManipulator( - $name, - function ( - $theService, $actualLocator, $extra - ) use ( - $services, $theService1, $theService2 - ) { - PHPUnit_Framework_Assert::assertSame( $theService1, $theService ); - PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); - PHPUnit_Framework_Assert::assertSame( 'Foo', $extra ); - return $theService2; - } - ); - - // force instantiation, check result - $this->assertSame( $theService2, $services->getService( $name ) ); - } - - public function testAddServiceManipulator_fail_undefined() { - $services = $this->newServiceContainer(); - - $theService = new stdClass(); - $name = 'TestService92834576'; - - $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class ); - - $services->addServiceManipulator( $name, function () use ( $theService ) { - return $theService; - } ); - } - - public function testAddServiceManipulator_fail_in_use() { - $services = $this->newServiceContainer( [ 'Foo' ] ); - - $theService = new stdClass(); - $name = 'TestService92834576'; - - $services->defineService( $name, function () use ( $theService ) { - return $theService; - } ); - - // create the service, so it can no longer be redefined - $services->getService( $name ); - - $this->setExpectedException( Wikimedia\Services\CannotReplaceActiveServiceException::class ); - - $services->addServiceManipulator( $name, function () { - return 'Foo'; - } ); - } - - public function testDisableService() { - $services = $this->newServiceContainer( [ 'Foo' ] ); - - $destructible = $this->getMockBuilder( Wikimedia\Services\DestructibleService::class ) - ->getMock(); - $destructible->expects( $this->once() ) - ->method( 'destroy' ); - - $services->defineService( 'Foo', function () use ( $destructible ) { - return $destructible; - } ); - $services->defineService( 'Bar', function () { - return new stdClass(); - } ); - $services->defineService( 'Qux', function () { - return new stdClass(); - } ); - - // instantiate Foo and Bar services - $services->getService( 'Foo' ); - $services->getService( 'Bar' ); - - // disable service, should call destroy() once. - $services->disableService( 'Foo' ); - - // disabled service should still be listed - $this->assertContains( 'Foo', $services->getServiceNames() ); - - // getting other services should still work - $services->getService( 'Bar' ); - - // disable non-destructible service, and not-yet-instantiated service - $services->disableService( 'Bar' ); - $services->disableService( 'Qux' ); - - $this->assertNull( $services->peekService( 'Bar' ) ); - $this->assertNull( $services->peekService( 'Qux' ) ); - - // disabled service should still be listed - $this->assertContains( 'Bar', $services->getServiceNames() ); - $this->assertContains( 'Qux', $services->getServiceNames() ); - - $this->setExpectedException( Wikimedia\Services\ServiceDisabledException::class ); - $services->getService( 'Qux' ); - } - - public function testDisableService_fail_undefined() { - $services = $this->newServiceContainer(); - - $theService = new stdClass(); - $name = 'TestService92834576'; - - $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class ); - - $services->redefineService( $name, function () use ( $theService ) { - return $theService; - } ); - } - - public function testDestroy() { - $services = $this->newServiceContainer(); - - $destructible = $this->getMockBuilder( Wikimedia\Services\DestructibleService::class ) - ->getMock(); - $destructible->expects( $this->once() ) - ->method( 'destroy' ); - - $services->defineService( 'Foo', function () use ( $destructible ) { - return $destructible; - } ); - - $services->defineService( 'Bar', function () { - return new stdClass(); - } ); - - // create the service - $services->getService( 'Foo' ); - - // destroy the container - $services->destroy(); - - $this->setExpectedException( Wikimedia\Services\ContainerDisabledException::class ); - $services->getService( 'Bar' ); - } - -} diff --git a/tests/phpunit/includes/libs/services/TestWiring1.php b/tests/phpunit/includes/libs/services/TestWiring1.php deleted file mode 100644 index b6ff4eb3b4..0000000000 --- a/tests/phpunit/includes/libs/services/TestWiring1.php +++ /dev/null @@ -1,10 +0,0 @@ - function () { - return 'Foo!'; - }, -]; diff --git a/tests/phpunit/includes/libs/services/TestWiring2.php b/tests/phpunit/includes/libs/services/TestWiring2.php deleted file mode 100644 index dfff64f048..0000000000 --- a/tests/phpunit/includes/libs/services/TestWiring2.php +++ /dev/null @@ -1,10 +0,0 @@ - function () { - return 'Bar!'; - }, -]; diff --git a/tests/phpunit/unit/includes/libs/services/ServiceContainerTest.php b/tests/phpunit/unit/includes/libs/services/ServiceContainerTest.php new file mode 100644 index 0000000000..f9e820ac6b --- /dev/null +++ b/tests/phpunit/unit/includes/libs/services/ServiceContainerTest.php @@ -0,0 +1,523 @@ +newServiceContainer(); + $names = $services->getServiceNames(); + + $this->assertInternalType( 'array', $names ); + $this->assertEmpty( $names ); + + $name = 'TestService92834576'; + $services->defineService( $name, function () { + return null; + } ); + + $names = $services->getServiceNames(); + $this->assertContains( $name, $names ); + } + + public function testHasService() { + $services = $this->newServiceContainer(); + + $name = 'TestService92834576'; + $this->assertFalse( $services->hasService( $name ) ); + + $services->defineService( $name, function () { + return null; + } ); + + $this->assertTrue( $services->hasService( $name ) ); + } + + public function testGetService() { + $services = $this->newServiceContainer( [ 'Foo' ] ); + + $theService = new stdClass(); + $name = 'TestService92834576'; + $count = 0; + + $services->defineService( + $name, + function ( $actualLocator, $extra ) use ( $services, $theService, &$count ) { + $count++; + PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); + PHPUnit_Framework_Assert::assertSame( $extra, 'Foo' ); + return $theService; + } + ); + + $this->assertSame( $theService, $services->getService( $name ) ); + + $services->getService( $name ); + $this->assertSame( 1, $count, 'instantiator should be called exactly once!' ); + } + + public function testGetServiceRecursionCheck() { + $services = $this->newServiceContainer(); + + $services->defineService( 'service1', function ( ServiceContainer $services ) { + $services->getService( 'service2' ); + } ); + + $services->defineService( 'service2', function ( ServiceContainer $services ) { + $services->getService( 'service3' ); + } ); + + $services->defineService( 'service3', function ( ServiceContainer $services ) { + $services->getService( 'service1' ); + } ); + + $exceptionThrown = false; + try { + $services->getService( 'service1' ); + } catch ( RuntimeException $e ) { + $exceptionThrown = true; + $this->assertSame( 'Circular dependency when creating service! ' . + 'service1 -> service2 -> service3 -> service1', $e->getMessage() ); + } + $this->assertTrue( $exceptionThrown, 'RuntimeException must be thrown' ); + } + + public function testGetService_fail_unknown() { + $services = $this->newServiceContainer(); + + $name = 'TestService92834576'; + + $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class ); + + $services->getService( $name ); + } + + public function testPeekService() { + $services = $this->newServiceContainer(); + + $services->defineService( + 'Foo', + function () { + return new stdClass(); + } + ); + + $services->defineService( + 'Bar', + function () { + return new stdClass(); + } + ); + + // trigger instantiation of Foo + $services->getService( 'Foo' ); + + $this->assertInternalType( + 'object', + $services->peekService( 'Foo' ), + 'Peek should return the service object if it had been accessed before.' + ); + + $this->assertNull( + $services->peekService( 'Bar' ), + 'Peek should return null if the service was never accessed.' + ); + } + + public function testPeekService_fail_unknown() { + $services = $this->newServiceContainer(); + + $name = 'TestService92834576'; + + $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class ); + + $services->peekService( $name ); + } + + public function testDefineService() { + $services = $this->newServiceContainer(); + + $theService = new stdClass(); + $name = 'TestService92834576'; + + $services->defineService( $name, function ( $actualLocator ) use ( $services, $theService ) { + PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); + return $theService; + } ); + + $this->assertTrue( $services->hasService( $name ) ); + $this->assertSame( $theService, $services->getService( $name ) ); + } + + public function testDefineService_fail_duplicate() { + $services = $this->newServiceContainer(); + + $theService = new stdClass(); + $name = 'TestService92834576'; + + $services->defineService( $name, function () use ( $theService ) { + return $theService; + } ); + + $this->setExpectedException( Wikimedia\Services\ServiceAlreadyDefinedException::class ); + + $services->defineService( $name, function () use ( $theService ) { + return $theService; + } ); + } + + public function testApplyWiring() { + $services = $this->newServiceContainer(); + + $wiring = [ + 'Foo' => function () { + return 'Foo!'; + }, + 'Bar' => function () { + return 'Bar!'; + }, + ]; + + $services->applyWiring( $wiring ); + + $this->assertSame( 'Foo!', $services->getService( 'Foo' ) ); + $this->assertSame( 'Bar!', $services->getService( 'Bar' ) ); + } + + public function testImportWiring() { + $services = $this->newServiceContainer(); + + $wiring = [ + 'Foo' => function () { + return 'Foo!'; + }, + 'Bar' => function () { + return 'Bar!'; + }, + 'Car' => function () { + return 'FUBAR!'; + }, + ]; + + $services->applyWiring( $wiring ); + + $services->addServiceManipulator( 'Foo', function ( $service ) { + return $service . '+X'; + } ); + + $services->addServiceManipulator( 'Car', function ( $service ) { + return $service . '+X'; + } ); + + $newServices = $this->newServiceContainer(); + + // create a service with manipulator + $newServices->defineService( 'Foo', function () { + return 'Foo!'; + } ); + + $newServices->addServiceManipulator( 'Foo', function ( $service ) { + return $service . '+Y'; + } ); + + // create a service before importing, so we can later check that + // existing service instances survive importWiring() + $newServices->defineService( 'Car', function () { + return 'Car!'; + } ); + + // force instantiation + $newServices->getService( 'Car' ); + + // Define another service, so we can later check that extra wiring + // is not lost. + $newServices->defineService( 'Xar', function () { + return 'Xar!'; + } ); + + // import wiring, but skip `Bar` + $newServices->importWiring( $services, [ 'Bar' ] ); + + $this->assertNotContains( 'Bar', $newServices->getServiceNames(), 'Skip `Bar` service' ); + $this->assertSame( 'Foo!+Y+X', $newServices->getService( 'Foo' ) ); + + // import all wiring, but preserve existing service instance + $newServices->importWiring( $services ); + + $this->assertContains( 'Bar', $newServices->getServiceNames(), 'Import all services' ); + $this->assertSame( 'Bar!', $newServices->getService( 'Bar' ) ); + $this->assertSame( 'Car!', $newServices->getService( 'Car' ), 'Use existing service instance' ); + $this->assertSame( 'Xar!', $newServices->getService( 'Xar' ), 'Predefined services are kept' ); + } + + public function testLoadWiringFiles() { + $services = $this->newServiceContainer(); + + $wiringFiles = [ + __DIR__ . '/TestWiring1.php', + __DIR__ . '/TestWiring2.php', + ]; + + $services->loadWiringFiles( $wiringFiles ); + + $this->assertSame( 'Foo!', $services->getService( 'Foo' ) ); + $this->assertSame( 'Bar!', $services->getService( 'Bar' ) ); + } + + public function testLoadWiringFiles_fail_duplicate() { + $services = $this->newServiceContainer(); + + $wiringFiles = [ + __DIR__ . '/TestWiring1.php', + __DIR__ . '/./TestWiring1.php', + ]; + + // loading the same file twice should fail, because + $this->setExpectedException( Wikimedia\Services\ServiceAlreadyDefinedException::class ); + + $services->loadWiringFiles( $wiringFiles ); + } + + public function testRedefineService() { + $services = $this->newServiceContainer( [ 'Foo' ] ); + + $theService1 = new stdClass(); + $name = 'TestService92834576'; + + $services->defineService( $name, function () { + PHPUnit_Framework_Assert::fail( + 'The original instantiator function should not get called' + ); + } ); + + // redefine before instantiation + $services->redefineService( + $name, + function ( $actualLocator, $extra ) use ( $services, $theService1 ) { + PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); + PHPUnit_Framework_Assert::assertSame( 'Foo', $extra ); + return $theService1; + } + ); + + // force instantiation, check result + $this->assertSame( $theService1, $services->getService( $name ) ); + } + + public function testRedefineService_disabled() { + $services = $this->newServiceContainer( [ 'Foo' ] ); + + $theService1 = new stdClass(); + $name = 'TestService92834576'; + + $services->defineService( $name, function () { + return 'Foo'; + } ); + + // disable the service. we should be able to redefine it anyway. + $services->disableService( $name ); + + $services->redefineService( $name, function () use ( $theService1 ) { + return $theService1; + } ); + + // force instantiation, check result + $this->assertSame( $theService1, $services->getService( $name ) ); + } + + public function testRedefineService_fail_undefined() { + $services = $this->newServiceContainer(); + + $theService = new stdClass(); + $name = 'TestService92834576'; + + $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class ); + + $services->redefineService( $name, function () use ( $theService ) { + return $theService; + } ); + } + + public function testRedefineService_fail_in_use() { + $services = $this->newServiceContainer( [ 'Foo' ] ); + + $theService = new stdClass(); + $name = 'TestService92834576'; + + $services->defineService( $name, function () { + return 'Foo'; + } ); + + // create the service, so it can no longer be redefined + $services->getService( $name ); + + $this->setExpectedException( Wikimedia\Services\CannotReplaceActiveServiceException::class ); + + $services->redefineService( $name, function () use ( $theService ) { + return $theService; + } ); + } + + public function testAddServiceManipulator() { + $services = $this->newServiceContainer( [ 'Foo' ] ); + + $theService1 = new stdClass(); + $theService2 = new stdClass(); + $name = 'TestService92834576'; + + $services->defineService( + $name, + function ( $actualLocator, $extra ) use ( $services, $theService1 ) { + PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); + PHPUnit_Framework_Assert::assertSame( 'Foo', $extra ); + return $theService1; + } + ); + + $services->addServiceManipulator( + $name, + function ( + $theService, $actualLocator, $extra + ) use ( + $services, $theService1, $theService2 + ) { + PHPUnit_Framework_Assert::assertSame( $theService1, $theService ); + PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); + PHPUnit_Framework_Assert::assertSame( 'Foo', $extra ); + return $theService2; + } + ); + + // force instantiation, check result + $this->assertSame( $theService2, $services->getService( $name ) ); + } + + public function testAddServiceManipulator_fail_undefined() { + $services = $this->newServiceContainer(); + + $theService = new stdClass(); + $name = 'TestService92834576'; + + $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class ); + + $services->addServiceManipulator( $name, function () use ( $theService ) { + return $theService; + } ); + } + + public function testAddServiceManipulator_fail_in_use() { + $services = $this->newServiceContainer( [ 'Foo' ] ); + + $theService = new stdClass(); + $name = 'TestService92834576'; + + $services->defineService( $name, function () use ( $theService ) { + return $theService; + } ); + + // create the service, so it can no longer be redefined + $services->getService( $name ); + + $this->setExpectedException( Wikimedia\Services\CannotReplaceActiveServiceException::class ); + + $services->addServiceManipulator( $name, function () { + return 'Foo'; + } ); + } + + public function testDisableService() { + $services = $this->newServiceContainer( [ 'Foo' ] ); + + $destructible = $this->getMockBuilder( Wikimedia\Services\DestructibleService::class ) + ->getMock(); + $destructible->expects( $this->once() ) + ->method( 'destroy' ); + + $services->defineService( 'Foo', function () use ( $destructible ) { + return $destructible; + } ); + $services->defineService( 'Bar', function () { + return new stdClass(); + } ); + $services->defineService( 'Qux', function () { + return new stdClass(); + } ); + + // instantiate Foo and Bar services + $services->getService( 'Foo' ); + $services->getService( 'Bar' ); + + // disable service, should call destroy() once. + $services->disableService( 'Foo' ); + + // disabled service should still be listed + $this->assertContains( 'Foo', $services->getServiceNames() ); + + // getting other services should still work + $services->getService( 'Bar' ); + + // disable non-destructible service, and not-yet-instantiated service + $services->disableService( 'Bar' ); + $services->disableService( 'Qux' ); + + $this->assertNull( $services->peekService( 'Bar' ) ); + $this->assertNull( $services->peekService( 'Qux' ) ); + + // disabled service should still be listed + $this->assertContains( 'Bar', $services->getServiceNames() ); + $this->assertContains( 'Qux', $services->getServiceNames() ); + + $this->setExpectedException( Wikimedia\Services\ServiceDisabledException::class ); + $services->getService( 'Qux' ); + } + + public function testDisableService_fail_undefined() { + $services = $this->newServiceContainer(); + + $theService = new stdClass(); + $name = 'TestService92834576'; + + $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class ); + + $services->redefineService( $name, function () use ( $theService ) { + return $theService; + } ); + } + + public function testDestroy() { + $services = $this->newServiceContainer(); + + $destructible = $this->getMockBuilder( Wikimedia\Services\DestructibleService::class ) + ->getMock(); + $destructible->expects( $this->once() ) + ->method( 'destroy' ); + + $services->defineService( 'Foo', function () use ( $destructible ) { + return $destructible; + } ); + + $services->defineService( 'Bar', function () { + return new stdClass(); + } ); + + // create the service + $services->getService( 'Foo' ); + + // destroy the container + $services->destroy(); + + $this->setExpectedException( Wikimedia\Services\ContainerDisabledException::class ); + $services->getService( 'Bar' ); + } + +} diff --git a/tests/phpunit/unit/includes/libs/services/TestWiring1.php b/tests/phpunit/unit/includes/libs/services/TestWiring1.php new file mode 100644 index 0000000000..b6ff4eb3b4 --- /dev/null +++ b/tests/phpunit/unit/includes/libs/services/TestWiring1.php @@ -0,0 +1,10 @@ + function () { + return 'Foo!'; + }, +]; diff --git a/tests/phpunit/unit/includes/libs/services/TestWiring2.php b/tests/phpunit/unit/includes/libs/services/TestWiring2.php new file mode 100644 index 0000000000..dfff64f048 --- /dev/null +++ b/tests/phpunit/unit/includes/libs/services/TestWiring2.php @@ -0,0 +1,10 @@ + function () { + return 'Bar!'; + }, +];