diff --git a/CHANGELOG.md b/CHANGELOG.md index 866e8d7..3a90348 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## 3.0.0-beta7 2019-11-03 + +#### Added + +- Adds `Application::getState` which returns an enum `ApplicationState` signifying whether the Application is Started, +Stopped, or Crashed. + +#### Changed + +- The DependencyGraph object now expects you to provide a Logger implementation as a constructor dependency instead of +the DependencyGraph creating the Logger object based off of a configuration. + +#### Removed + +- Removed the Configuration interface and corresponding ConfigurationFactory. In practice this Configuration was +tied to a process for providing an out-of-the-box solution for invoking Applications that was clunky and not well +thought out. For now instead of moving forward with a sub-optimal solution each app will need to provide its own +boilerplate for executing the app. As more experience is gathered in running real-life apps on this framework we +may revisit the Configuration concept. +- Removed the shell script that created a rough app skeleton. More thought needs to go into how this would work before +it is released live. + ## 3.0.0-beta6 2019-11-02 #### Fixed diff --git a/bin/labrador-app-skeleton b/bin/labrador-app-skeleton deleted file mode 100755 index 03c83a2..0000000 --- a/bin/labrador-app-skeleton +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env php - $appNamespace -]; - -$phpPrefix = '' . PHP_EOL; - -$files = [ - [ - 'template' => 'injector_provider', - 'outputPath' => $workingDir . '/resources/config/injector_provider.php', - 'variables' => $globalTemplateVars, - 'prefix' => $phpPrefix - ], - [ - 'template' => 'xml_configuration', - 'outputPath' => $workingDir . '/resources/config/labrador_configuration.xml', - 'variables' => array_merge([], $globalTemplateVars, [ - 'appNamespace' => str_replace('\\', '.', $appNamespace), - 'injectorProviderPath' => 'resources/config/injector_provider.php' - ]), - 'prefix' => $xmlPrefix - ], - [ - 'template' => 'application', - 'outputPath' => $workingDir . '/src/Application.php', - 'variables' => $globalTemplateVars, - 'prefix' => $phpPrefix - ], - [ - 'template' => 'application_test', - 'outputPath' => $workingDir . '/test/ApplicationTest.php', - 'variables' => $globalTemplateVars, - 'prefix' => $phpPrefix - ], - [ - 'template' => 'dependency_graph', - 'outputPath' => $workingDir . '/src/DependencyGraph.php', - 'variables' => $globalTemplateVars, - 'prefix' => $phpPrefix - ], - [ - 'template' => 'dependency_graph_test', - 'outputPath' => $workingDir . '/test/DependencyGraphTest.php', - 'variables' => $globalTemplateVars, - 'prefix' => $phpPrefix - ], - [ - 'template' => 'readme', - 'outputPath' => $workingDir . '/README.md', - 'variables' => $globalTemplateVars, - 'prefix' => '' - ], - [ - 'template' => 'phpunit.xml', - 'outputPath' => $workingDir . '/phpunit.xml.dist', - 'variables' => $globalTemplateVars, - 'prefix' => $xmlPrefix - ] -]; - -foreach ($files as $file) { - $contents = $templateRenderer($file['template'], $file['variables']); - if (!file_exists($file['outputPath'])) { - file_put_contents($file['outputPath'], $file['prefix'] . $contents); - } -} - -echo "Would you like to have unit testing framework installed? Y/n: "; -$response = trim(fgets(STDIN)); - -if (empty($response) || strtolower($response) === 'y') { - exec('composer require --dev phpunit/phpunit amphp/phpunit-util:dev-master'); -} \ No newline at end of file diff --git a/resources/schemas/configuration.schema.json b/resources/schemas/configuration.schema.json deleted file mode 100644 index 6c9ef63..0000000 --- a/resources/schemas/configuration.schema.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "http://labrador-kennel.cspray.io/schemas/configuration.schema.json", - "title": "Labrador Configuration", - "description": "Schema to ensure that a JSON based configuration provides valid values.", - "type": "object", - "properties": { - "labrador": { - "type": "object", - "properties": { - "logging": { - "type": "object", - "properties": { - "name": { - "description": "The name of your application's logs", - "type": "string", - "minLength": 1 - }, - "outputPath": { - "description": "The resource path that your logs will stream to.", - "type": "string", - "minLength": 1 - } - }, - "required": [ - "name", - "outputPath" - ] - }, - "injectorProviderPath": { - "description": "A file path that returns a callback that accepts a Configuration instance and returns an Injector.", - "type": "string" - }, - "plugins": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "logging", - "injectorProviderPath", - "plugins" - ] - } - }, - "required": ["labrador"] -} \ No newline at end of file diff --git a/resources/schemas/configuration.schema.xsd b/resources/schemas/configuration.schema.xsd deleted file mode 100644 index 067d6c7..0000000 --- a/resources/schemas/configuration.schema.xsd +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/resources/templates/app_skeleton/application.php b/resources/templates/app_skeleton/application.php deleted file mode 100644 index 93aece6..0000000 --- a/resources/templates/app_skeleton/application.php +++ /dev/null @@ -1,20 +0,0 @@ -namespace ; - -use Cspray\Labrador\AbstractApplication; -use Amp\Promise; -use function Amp\call; - -/** - * - * @package - * @license See LICENSE in source root - */ -class Application extends AbstractApplication { - - public function execute() : Promise { - return call(function() { - // Execute your Application logic here - }); - } - -} diff --git a/resources/templates/app_skeleton/application_test.php b/resources/templates/app_skeleton/application_test.php deleted file mode 100644 index ed4ae4b..0000000 --- a/resources/templates/app_skeleton/application_test.php +++ /dev/null @@ -1,17 +0,0 @@ -namespace \Test; - -use \Application; -use Cspray\Labrador\Application as LabradorApplication; -use Amp\PHPUnit\AsyncTestCase; - -class ApplicationTest extends AsyncTestCase { - - public function testApplicationInstantiable() { - $application = new Application(); - - $this->assertInstanceOf(LabradorApplication::class, $application); - } - - // You should populate this with more (and better) tests! - -} \ No newline at end of file diff --git a/resources/templates/app_skeleton/dependency_graph.php b/resources/templates/app_skeleton/dependency_graph.php deleted file mode 100644 index b8448cf..0000000 --- a/resources/templates/app_skeleton/dependency_graph.php +++ /dev/null @@ -1,22 +0,0 @@ -namespace ; - -use Auryn\Injector; -use Cspray\Labrador\Application as LabradorApplication; - -/** - * - * @package - * @license See LICENSE in source root - */ -class DependencyGraph { - - public function wireObjectGraph(Injector $injector = null) : Injector { - $injector = $injector ?? new Injector(); - - $injector->share(Application::class); - $injector->alias(LabradorApplication::class, Application::class); - - return $injector; - } - -} diff --git a/resources/templates/app_skeleton/dependency_graph_test.php b/resources/templates/app_skeleton/dependency_graph_test.php deleted file mode 100644 index ec46f96..0000000 --- a/resources/templates/app_skeleton/dependency_graph_test.php +++ /dev/null @@ -1,18 +0,0 @@ -namespace \Test; - -use \DependencyGraph; -use Cspray\Labrador\Application as LabradorApplication; -use Amp\PHPUnit\AsyncTestCase; - -class ApplicationTest extends AsyncTestCase { - - public function testApplicationCreatedForLabradorApplication() { - $injector = (new DependencyGraph())->wireObject(); - $application = $injector->make(LabradorApplication::class); - - $this->assertInstanceOf(Application::class, $application); - } - - // You should populate this with more (and better) tests! - -} diff --git a/resources/templates/app_skeleton/injector_provider.php b/resources/templates/app_skeleton/injector_provider.php deleted file mode 100644 index 0046da4..0000000 --- a/resources/templates/app_skeleton/injector_provider.php +++ /dev/null @@ -1,9 +0,0 @@ -namespace ; - -use Cspray\Labrador\Configuration as LabradorConfiguration; -use Cspray\Labrador\DependencyGraph as LabradorDependencyGraph; - -return function(LabradorConfiguration configuration) { - $injector = (new LabradorDependencyGraph($configuration))->wireObjectGraph(); - return (new DependencyGraph())->wireObjectGraph($injector); -}; diff --git a/resources/templates/app_skeleton/phpunit.xml.php b/resources/templates/app_skeleton/phpunit.xml.php deleted file mode 100644 index cc34862..0000000 --- a/resources/templates/app_skeleton/phpunit.xml.php +++ /dev/null @@ -1,36 +0,0 @@ - - - - ./test - ./test - - - - - - ./src - - - diff --git a/resources/templates/app_skeleton/readme.php b/resources/templates/app_skeleton/readme.php deleted file mode 100644 index 752ad20..0000000 --- a/resources/templates/app_skeleton/readme.php +++ /dev/null @@ -1,5 +0,0 @@ -# - -An amazing application written on top of [Labrador]! - -[Labrador]: https://labrador-kennel.io diff --git a/resources/templates/app_skeleton/xml_configuration.php b/resources/templates/app_skeleton/xml_configuration.php deleted file mode 100644 index de50e88..0000000 --- a/resources/templates/app_skeleton/xml_configuration.php +++ /dev/null @@ -1,10 +0,0 @@ - - dev - - - php://stdout - - - - - diff --git a/src/AbstractApplication.php b/src/AbstractApplication.php index 8b8300e..f257672 100644 --- a/src/AbstractApplication.php +++ b/src/AbstractApplication.php @@ -2,7 +2,11 @@ namespace Cspray\Labrador; +use Amp\Deferred; +use Amp\Loop; use Amp\Promise; +use Amp\Success; +use Cspray\Labrador\Exception\InvalidStateException; use Cspray\Labrador\Plugin\Pluggable; use Cspray\Labrador\Plugin\Plugin; use Psr\Log\LoggerAwareTrait; @@ -29,12 +33,70 @@ abstract class AbstractApplication implements Application { use LoggerAwareTrait; + /** + * @var Pluggable + */ private $pluggable; + /** + * @var Deferred + */ + private $deferred; + + /** + * @var ApplicationState + */ + private $state; + public function __construct(Pluggable $pluggable) { $this->pluggable = $pluggable; + $this->state = ApplicationState::Stopped(); } + public function start() : Promise { + if (!$this->state->equals(ApplicationState::Stopped())) { + $msg = 'Application must be in a Stopped state to start but it\'s current state is %s'; + throw new InvalidStateException(sprintf($msg, $this->state->toString())); + } + $this->deferred = new Deferred(); + + $this->state = ApplicationState::Started(); + $this->doStart()->onResolve(function($err) { + // This ensures we properly handle the case where doStart may return a Promise that resolves immediately + // In such a case we would call resolveDeferred(), which sets $deferred to null, before we actually + // returned the Promise for the $deferred required by Application::start(). By resolving the Promise for + // when this is done executing on the next tick of the event loop we ensure there's actually something to + // return for the start() method. + Loop::defer(function() use($err) { + $this->resolveDeferred($err); + }); + }); + + return $this->deferred->promise(); + } + + public function stop() : Promise { + $this->resolveDeferred(); + return new Success(); + } + + private function resolveDeferred(Throwable $throwable = null) : void { + if (isset($throwable)) { + $this->state = ApplicationState::Crashed(); + $this->deferred->fail($throwable); + } else { + $this->state = ApplicationState::Stopped(); + $this->deferred->resolve(); + } + $this->deferred = null; + } + + public function getState() : ApplicationState { + return $this->state; + } + + abstract protected function doStart() : Promise; + /** * This implementation does nothing by default, logging of the exception itself for Labrador's purposes are handled * within the Engine and there's nothing more we can do. @@ -44,7 +106,7 @@ public function __construct(Pluggable $pluggable) { * * @param Throwable $throwable */ - public function exceptionHandler(Throwable $throwable) : void { + public function handleException(Throwable $throwable) : void { // noop } diff --git a/src/AmpEngine.php b/src/AmpEngine.php index a3ab5bb..7583231 100644 --- a/src/AmpEngine.php +++ b/src/AmpEngine.php @@ -86,7 +86,7 @@ public function run(Application $application) : void { ), ['file' => $error->getFile(), 'line' => $error->getLine()] ); - $application->exceptionHandler($error); + $application->handleException($error); Loop::defer(function() use($application) { $this->logger->info('Starting Application cleanup process.'); yield $this->emitEngineShutDownEvent($application); @@ -111,7 +111,7 @@ public function run(Application $application) : void { } $this->logger->info('Starting Application process.'); - yield $application->execute(); + yield $application->start(); $this->logger->info('Completed Application process.'); $this->logger->info('Starting Application cleanup process.'); diff --git a/src/Application.php b/src/Application.php index 1c9f620..9108d18 100644 --- a/src/Application.php +++ b/src/Application.php @@ -17,15 +17,40 @@ interface Application extends Pluggable, LoggerAwareInterface { /** - * Perform whatever logic or operations your application requires; return a Promise that resolves when you app is - * finished running. + * Start running your Application, performing whatever logic is necessary for the given implementation. + * + * Resolve the Promise either when the Application has naturally reached a stopping point OR when the + * Application::stop is called. For some types of long-running Applications it is expected that this Promise will + * not resolve unless Application::stop is explicitly invoked. + * + * If start() is called successive times without allowing the Promises to resolve as the method is invoked an + * InvalidStateException MUST be thrown. It is not expected nor will it be supported that an Application may start + * after it has been started and before it has been stopped. * * This method should avoid throwing an exception and instead fail the Promise with the Exception that caused the * application to crash. * * @return Promise */ - public function execute() : Promise; + public function start() : Promise; + + /** + * Force the Application to stop running; the Promise returned allows the Application to potentially gracefully + * handle any remaining tasks. + * + * The Promise returned from Application::start() MUST resolve before this Promise resolves or your Application may + * enter a state where it cannot be stopped without forcefully killing the process. + * + * @return Promise + */ + public function stop() : Promise; + + /** + * Return the state in which the Application is currently in. + * + * @return ApplicationState + */ + public function getState() : ApplicationState; /** * Handle an exception being thrown in your application; if you can gracefully handle the exception the app will @@ -34,5 +59,5 @@ public function execute() : Promise; * @param Throwable $throwable * @return void */ - public function exceptionHandler(Throwable $throwable) : void; + public function handleException(Throwable $throwable) : void; } diff --git a/src/ApplicationState.php b/src/ApplicationState.php new file mode 100644 index 0000000..ddb860c --- /dev/null +++ b/src/ApplicationState.php @@ -0,0 +1,28 @@ + */ - public function execute() : Promise { + protected function doStart() : Promise { return call($this->handler); } @@ -38,7 +38,7 @@ public function execute() : Promise { * @param Throwable $throwable * @return void */ - public function exceptionHandler(Throwable $throwable) : void { + public function handleException(Throwable $throwable) : void { if (isset($this->exceptionHandler)) { ($this->exceptionHandler)($throwable); } diff --git a/src/Configuration.php b/src/Configuration.php deleted file mode 100644 index 1e7e985..0000000 --- a/src/Configuration.php +++ /dev/null @@ -1,42 +0,0 @@ - - */ - public function getPlugins() : array; -} diff --git a/src/ConfigurationFactory.php b/src/ConfigurationFactory.php deleted file mode 100644 index 39cfbae..0000000 --- a/src/ConfigurationFactory.php +++ /dev/null @@ -1,189 +0,0 @@ -schemasDirectory = dirname(__DIR__, 1) . '/resources/schemas'; - } - - /** - * @param string $configurationPath - * @return Configuration - * @throws Exception\Exception - */ - public function createFromFilePath(string $configurationPath) : Configuration { - if (!file_exists($configurationPath)) { - throw Exceptions::createException(Exceptions::CONFIG_ERR_FILE_NOT_EXIST); - } - $extension = pathinfo($configurationPath, PATHINFO_EXTENSION); - if ($extension === 'json') { - $configData = $this->loadJsonConfigData($configurationPath); - } elseif ($extension === 'xml') { - $configData = $this->loadXmlConfigData($configurationPath); - } elseif ($extension === 'php') { - $configData = $this->loadPhpConfigData($configurationPath); - } else { - throw Exceptions::createException(Exceptions::CONFIG_ERR_FILE_UNSUPPORTED_EXTENSION); - } - - return $this->createConfiguration($configData); - } - - /** - * @param string $configurationPath - * @return stdClass - * @throws Exception\Exception - */ - private function loadJsonConfigData(string $configurationPath) : stdClass { - return $this->validateJsonSource(file_get_contents($configurationPath)); - } - - /** - * @param string $configurationPath - * @return stdClass - * @throws Exception\Exception - */ - private function loadXmlConfigData(string $configurationPath) : stdClass { - $xmlSchemaPath = $this->schemasDirectory . '/configuration.schema.xsd'; - libxml_use_internal_errors(true); - $dom = new DOMDocument(); - $dom->loadXML(file_get_contents($configurationPath)); - - if (!empty(libxml_get_errors()) || !$dom->schemaValidate($xmlSchemaPath)) { - $exception = Exceptions::createException(Exceptions::CONFIG_ERR_XML_INVALID_SCHEMA); - throw $exception; - } - - libxml_clear_errors(); - libxml_use_internal_errors(false); - - $xpath = new DOMXPath($dom); - $xpath->registerNamespace('l', 'https://labrador-kennel.io/core/schemas/configuration.schema.xsd'); - - $configData = new stdClass(); - - $configData->labrador = new stdClass(); - $configData->labrador->logging = new stdClass(); - $configData->labrador->logging->name = $xpath->evaluate('/l:labrador/l:logging/l:name/text()') - ->item(0)->nodeValue; - $configData->labrador->logging->outputPath = $xpath->evaluate('/l:labrador/l:logging/l:outputPath/text()') - ->item(0)->nodeValue; - - $configData->labrador->plugins = []; - foreach ($xpath->query('/l:labrador/l:plugins/l:plugin') as $pluginNode) { - $configData->labrador->plugins[] = $pluginNode->nodeValue; - } - - $configData->labrador->injectorProviderPath = $xpath->evaluate('/l:labrador/l:injectorProviderPath/text()') - ->item(0)->nodeValue; - - return $configData; - } - - /** - * @param string $configurationPath - * @return stdClass - * @throws Exception\Exception - */ - private function loadPhpConfigData(string $configurationPath) : stdClass { - $phpConfig = include $configurationPath; - if (is_array($phpConfig)) { - return $this->validateJsonSource(json_encode($phpConfig)); - } elseif (is_object($phpConfig) && $phpConfig instanceof Configuration) { - $configData = new stdClass(); - $configData->configuration = $phpConfig; - return $configData; - } else { - throw Exceptions::createException(Exceptions::CONFIG_ERR_PHP_INVALID_RETURN_TYPE); - } - } - - /** - * @param string $jsonSource - * @return stdClass - * @throws Exception\Exception - */ - private function validateJsonSource(string $jsonSource) : stdClass { - $jsonSchemaPath = $this->schemasDirectory . '/configuration.schema.json'; - $jsonSchema = Schema::fromJsonString(file_get_contents($jsonSchemaPath)); - $jsonValidator = new Validator(); - $configData = json_decode($jsonSource); - - $results = $jsonValidator->schemaValidation($configData, $jsonSchema); - - if (!$results->isValid()) { - $exception = Exceptions::createException(Exceptions::CONFIG_ERR_JSON_INVALID_SCHEMA); - throw $exception; - } - - return $configData; - } - - private function createConfiguration(stdClass $configData) { - if (isset($configData->configuration)) { - return $configData->configuration; - } - - $logName = $configData->labrador->logging->name; - $logPath = $configData->labrador->logging->outputPath; - $plugins = $configData->labrador->plugins; - $injectorProviderPath = $configData->labrador->injectorProviderPath; - - return new class( - $logName, - $logPath, - $plugins, - $injectorProviderPath - ) implements Configuration { - - private $logName; - private $logPath; - private $plugins; - private $injectorProviderPath; - - public function __construct( - string $logName, - string $logPath, - array $plugins, - string $injectorProviderPath - ) { - $this->logName = $logName; - $this->logPath = $logPath; - $this->plugins = $plugins; - $this->injectorProviderPath = $injectorProviderPath; - } - - public function getLogName() : string { - return $this->logName; - } - - public function getLogPath() : string { - return $this->logPath; - } - - public function getPlugins() : array { - return $this->plugins; - } - - public function getInjectorProviderPath() : string { - return $this->injectorProviderPath; - } - }; - } -} diff --git a/src/DependencyGraph.php b/src/DependencyGraph.php index ab352d6..41b4cea 100644 --- a/src/DependencyGraph.php +++ b/src/DependencyGraph.php @@ -27,10 +27,10 @@ */ final class DependencyGraph { - private $configuration; + private $logger; - public function __construct(Configuration $configuration) { - $this->configuration = $configuration; + public function __construct(LoggerInterface $logger) { + $this->logger = $logger; } /** @@ -56,9 +56,7 @@ public function wireObjectGraph(Injector $injector = null) : Injector { $injector->share(Engine::class); $injector->alias(Engine::class, AmpEngine::class); - $logger = new Logger($this->configuration->getLogName()); - $handler = new StreamHandler(new ResourceOutputStream(@fopen($this->configuration->getLogPath(), 'wb'))); - $logger->pushHandler($handler); + $logger = $this->logger; $injector->share($logger); $injector->alias(LoggerInterface::class, get_class($logger)); $injector->prepare(LoggerAwareInterface::class, function(LoggerAwareInterface $aware) use($logger) { diff --git a/test/AbstractApplicationTest.php b/test/AbstractApplicationTest.php index 83db938..407ea7e 100644 --- a/test/AbstractApplicationTest.php +++ b/test/AbstractApplicationTest.php @@ -2,24 +2,32 @@ namespace Cspray\Labrador\Test; +use Amp\Deferred; +use Amp\Delayed; +use Amp\Failure; +use Amp\Loop; +use Amp\PHPUnit\AsyncTestCase; use Amp\Success; use Cspray\Labrador\AbstractApplication; +use Cspray\Labrador\ApplicationState; +use Cspray\Labrador\Exception\InvalidStateException; use Cspray\Labrador\Plugin\Pluggable; use Cspray\Labrador\Test\Stub\PluginStub; -use PHPUnit\Framework\TestCase; +use function Amp\call; /** * * @package Cspray\Labrador\Test * @license See LICENSE in source root */ -class AbstractApplicationTest extends TestCase { +class AbstractApplicationTest extends AsyncTestCase { private $pluggable; /** @var AbstractApplication */ private $subject; public function setUp() : void { + parent::setUp(); $this->pluggable = $this->getMockBuilder(Pluggable::class)->getMock(); $this->subject = $this->getMockForAbstractClass(AbstractApplication::class, [$this->pluggable]); } @@ -104,4 +112,79 @@ public function testRegisterPluginRemoveHandlerDelegatedToPluggable() { $this->subject->registerPluginRemoveHandler('PluginClass', $handler, 1, 2, 'foo'); } + + public function testApplicationStartPromiseResolvesWhenStopCalled() { + $this->subject->expects($this->once())->method('doStart')->willReturn((new Deferred())->promise()); + $resolved = false; + $this->subject->start()->onResolve(function() use(&$resolved) { + $resolved = true; + }); + + $this->assertFalse($resolved); + + yield $this->subject->stop(); + + $this->assertTrue($resolved); + } + + public function testApplicationStartPromiseResolvesWhenDelegateResolves() { + return call(function() { + $this->subject->expects($this->once())->method('doStart')->willReturn(new Success()); + + yield $this->subject->start(); + + // we only get here if the Promise from start() resolves + $this->assertTrue(true); + }); + } + + public function testApplicationStateBeforeStartIsStopped() { + $this->assertSame(ApplicationState::Stopped(), $this->subject->getState()); + } + + public function testApplicationStateRespondsToStartingNaturallyStopping() { + $this->subject->expects($this->once())->method('doStart')->willReturn(new Success()); + $this->subject->start(); + + $this->assertSame(ApplicationState::Started(), $this->subject->getState()); + + yield new Delayed(0); + + $this->assertSame(ApplicationState::Stopped(), $this->subject->getState()); + } + + public function testApplicationStateRespondsToStartingExplicitlyStopping() { + $this->subject->expects($this->once())->method('doStart')->willReturn((new Deferred())->promise()); + $this->subject->start(); + + $this->assertSame(ApplicationState::Started(), $this->subject->getState()); + + yield $this->subject->stop(); + + $this->assertSame(ApplicationState::Stopped(), $this->subject->getState()); + } + + public function testApplicationStateFlipsToCrashedWhenDoStartFails() { + $exception = new \RuntimeException('Thrown from doStart'); + $this->subject->expects($this->once())->method('doStart')->willReturn(new Failure($exception)); + + try { + yield $this->subject->start(); + } catch (\RuntimeException $runtimeException) { + $this->assertSame(ApplicationState::Crashed(), $this->subject->getState()); + $this->assertSame($exception, $runtimeException); + } + } + + public function testApplicationStartSuccessiveTimesThrowsException() { + $this->subject->expects($this->once())->method('doStart')->willReturn(new Delayed(0)); + $this->subject->start(); + + $this->expectException(InvalidStateException::class); + $this->expectExceptionMessage( + 'Application must be in a Stopped state to start but it\'s current state is Started' + ); + + $this->subject->start(); + } } diff --git a/test/AmpEngineTest.php b/test/AmpEngineTest.php index 8a6840c..bd24cd8 100644 --- a/test/AmpEngineTest.php +++ b/test/AmpEngineTest.php @@ -59,14 +59,18 @@ private function getEngine(Emitter $eventEmitter = null) : AmpEngine { return $engine; } + private function mockPluggable() : Pluggable { + $pluggable = $this->getMockBuilder(Pluggable::class)->getMock(); + $pluggable->expects($this->once())->method('loadPlugins')->willReturn(new Success()); + return $pluggable; + } + private function noopApp() : Application { - return new NoopApplication(); + return new NoopApplication($this->mockPluggable()); } private function callbackApp(callable $callback) : Application { - $pluggable = $this->getMockBuilder(Pluggable::class)->getMock(); - $pluggable->expects($this->once())->method('loadPlugins')->willReturn(new Success()); - return new CallbackApplication($pluggable, $callback); + return new CallbackApplication($this->mockPluggable(), $callback); } private function exceptionHandlerApp(callable $appCallback, callable $handler) : Application { @@ -100,7 +104,7 @@ public function testEventsExecutedInOrder() { $engine->onEngineBootup($bootUpCb); $engine->onEngineShutdown($cleanupCb); - $engine->run(new NoopApplication()); + $engine->run($this->noopApp()); $this->assertSame([1,2,3,4,5,6], $data->data); } @@ -204,7 +208,7 @@ public function testCallingRunMultipleTimesThrowsException() { $data->data = $throwable; }; $appCb = function() use($engine) { - $engine->run($this->noopApp()); + $engine->run(new NoopApplication($this->getMockBuilder(Pluggable::class)->getMock())); return new Success(); }; $app = $this->exceptionHandlerApp($appCb, $handlerCb); @@ -233,7 +237,7 @@ public function testEngineStateDuringRunIsRunning() { public function testEngineStateAfterRunIsIdle() { $engine = $this->getEngine(); - $app = new NoopApplication(); + $app = $this->noopApp(); $engine->run($app); $this->assertSame(EngineState::Idle(), $engine->getState()); @@ -262,7 +266,7 @@ public function testEngineBootupEventCalledOnceOnMultipleRunCalls() { $engine = $this->getEngine(); $engine->run($this->noopApp()); - $engine->run($this->noopApp()); + $engine->run(new NoopApplication($this->getMockBuilder(Pluggable::class)->getMock())); $this->assertSame([1], $data->data); } @@ -289,7 +293,7 @@ public function testApplicationLoadPluginsCalled() { } public function testLogMessagesOnSuccessfulApplicationRunNoPlugins() { - $app = new NoopApplication(); + $app = $this->noopApp(); $engine = $this->getEngine(); $engine->run($app); diff --git a/test/CallbackApplicationTest.php b/test/CallbackApplicationTest.php index 764c877..fc2eff2 100644 --- a/test/CallbackApplicationTest.php +++ b/test/CallbackApplicationTest.php @@ -29,7 +29,7 @@ function() use(&$counter) { } ); - yield $subject->execute(); + yield $subject->start(); $this->assertSame(3, $counter); } @@ -47,7 +47,7 @@ function(\Throwable $throwable) use(&$exception) { } ); - $subject->exceptionHandler($exceptionToThrow); + $subject->handleException($exceptionToThrow); $this->assertSame($exception, $exceptionToThrow); } @@ -60,7 +60,7 @@ function() { } ); - $subject->exceptionHandler(new \RuntimeException()); + $subject->handleException(new \RuntimeException()); $this->assertTrue(true, 'Expected to not throw an error so if we get here everything is good'); } diff --git a/test/ConfigurationFactoryTest.php b/test/ConfigurationFactoryTest.php deleted file mode 100644 index 1d374c5..0000000 --- a/test/ConfigurationFactoryTest.php +++ /dev/null @@ -1,110 +0,0 @@ -subject = new ConfigurationFactory(); - } - - public function testFileDoesNotExistThrowsException() { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('The configuration provided is not a valid file path that can be read from.'); - - $this->subject->createFromFilePath('something_non_existent'); - } - - public function testFileIsNotSupportedTypeThrowsException() { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('The file extension for the provided configuration is not supported.'); - - $this->subject->createFromFilePath(__DIR__ . '/_data/invalid-configuration-file-extension.txt'); - } - - public function testInvalidJsonConfigurationThrowsException() { - $this->expectException(InvalidStateException::class); - $this->expectExceptionMessage("The configuration provided does not validate against the required JSON schema."); - - $this->subject->createFromFilePath(__DIR__ . '/_data/invalid-json-configuration.json'); - } - - public function testValidJsonConfigurationReturnsCorrectValues() { - $config = $this->subject->createFromFilePath(__DIR__ . '/_data/valid-json-configuration.json'); - - $this->assertSame([PluginStub::class], $config->getPlugins()); - $this->assertSame("test-env", $config->getLogName()); - $this->assertSame("php://stdout", $config->getLogPath()); - $this->assertSame('file://yadda/yadda', $config->getInjectorProviderPath()); - } - - public function testInvalidXmlConfigurationThrowsException() { - $this->expectException(InvalidStateException::class); - $this->expectExceptionMessage("The configuration provided does not validate against the required XML schema."); - - $this->subject->createFromFilePath(__DIR__ . '/_data/invalid-xml-configuration.xml'); - } - - public function testValidXmlConfigurationReturnsCorrectValues() { - $config = $this->subject->createFromFilePath(__DIR__ . '/_data/valid-xml-configuration.xml'); - - $this->assertSame("xml-config-log", $config->getLogName()); - $this->assertSame("php://stdout", $config->getLogPath()); - $this->assertSame([PluginStub::class], $config->getPlugins()); - $this->assertSame('file://yadda/yadda/xml', $config->getInjectorProviderPath()); - } - - public function testInvalidPhpReturnTypeThrowsException() { - $this->expectException(InvalidStateException::class); - $msg = 'The configuration provided does not return a valid PHP array or ' . Configuration::class . ' instance'; - $this->expectExceptionMessage($msg); - - $this->subject->createFromFilePath(__DIR__ . '/_data/invalid-return-type-php-configuration.php'); - } - - public function testInvalidPhpArraySchemaThrowsException() { - $this->expectException(InvalidStateException::class); - $this->expectExceptionMessage("The configuration provided does not validate against the required JSON schema."); - - $this->subject->createFromFilePath(__DIR__ . '/_data/invalid-schema-php-configuration.php'); - } - - public function testValidPhpArraySchemaHasCorrectData() { - $config = $this->subject->createFromFilePath(__DIR__ . '/_data/valid-array-php-configuration.php'); - - $this->assertSame('php-array-log', $config->getLogName()); - $this->assertSame('php://stdout', $config->getLogPath()); - $this->assertSame([PluginStub::class, FooPluginDependentStub::class], $config->getPlugins()); - $this->assertSame('file://yadda/yadda/array', $config->getInjectorProviderPath()); - } - - public function testValidPhpConfigurationHasCorrectData() { - $path = __DIR__ . '/_data/valid-configuration-instance-php-configuration.php'; - $config = $this->subject->createFromFilePath($path); - - $this->assertSame('php-instance-log', $config->getLogName()); - $this->assertSame('php://stdout', $config->getLogPath()); - $this->assertSame('file://yadda/yadda/instance', $config->getInjectorProviderPath()); - $this->assertSame([PluginStub::class, FooPluginStub::class], $config->getPlugins()); - } -} diff --git a/test/DependencyGraphTest.php b/test/DependencyGraphTest.php index 62080dc..5113e92 100644 --- a/test/DependencyGraphTest.php +++ b/test/DependencyGraphTest.php @@ -5,7 +5,6 @@ use Amp\Log\StreamHandler; use Amp\PHPUnit\AsyncTestCase; use Cspray\Labrador\AmpEngine; -use Cspray\Labrador\Configuration; use Cspray\Labrador\DependencyGraph; use Auryn\Injector; use Cspray\Labrador\Engine; @@ -14,89 +13,50 @@ use Cspray\Labrador\Test\Stub\LoggerAwareStub; use Monolog\Logger; use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; class DependencyGraphTest extends AsyncTestCase { - private function getConfiguration() : Configuration { - return new class implements Configuration { - - /** - * Return the name of the log that Monolog will use to identify log messages from this Application. - * - * @return string - */ - public function getLogName() : string { - return 'dependency-graph-test'; - } - - /** - * Return a path that can be used as a resource stream to write log messages to. - * - * @return string - */ - public function getLogPath() : string { - return 'php://memory'; - } - - /** - * Return a path in which the file returns a callable that accepts a single Configuration instance and - * returns an Injector. - * - * @return string - */ - public function getInjectorProviderPath() : string { - throw new \RuntimeException('Did not expect this to be called'); - } - - /** - * Return a Set of fully qualified class names for the Plugins that should be added to your Application. - * - * @return Set - */ - public function getPlugins() : array { - throw new \RuntimeException('Did not expect this to be called'); - } - }; - } + private $logger; public function testInjectorInstanceCreated() { - $injector = (new DependencyGraph($this->getConfiguration()))->wireObjectGraph(); + $injector = $this->getInjector(); $this->assertInstanceOf(Injector::class, $injector); } public function testInjectorIsNotShared() { - $injector = (new DependencyGraph($this->getConfiguration()))->wireObjectGraph(); + $injector = $this->getInjector(); $this->assertNotSame($injector, $injector->make(Injector::class)); } public function testEngineAliasedToAmpEngine() { - $injector = (new DependencyGraph($this->getConfiguration()))->wireObjectGraph(); + $injector = $this->getInjector(); $this->assertInstanceOf(AmpEngine::class, $injector->make(Engine::class)); } public function testEngineShared() { - $injector = (new DependencyGraph($this->getConfiguration()))->wireObjectGraph(); + $injector = $this->getInjector(); $this->assertSame($injector->make(Engine::class), $injector->make(Engine::class)); } public function testPluggableAliasedToPluginManager() { - $injector = (new DependencyGraph($this->getConfiguration()))->wireObjectGraph(); + $injector = $this->getInjector(); $this->assertInstanceOf(PluginManager::class, $injector->make(Pluggable::class)); } public function testPluggableShared() { - $injector = (new DependencyGraph($this->getConfiguration()))->wireObjectGraph(); + $injector = $this->getInjector(); $this->assertSame($injector->make(Pluggable::class), $injector->make(Pluggable::class)); } public function testPluginManagerGetsCorrectInjector() { - $injector = (new DependencyGraph($this->getConfiguration()))->wireObjectGraph(); + $injector = $this->getInjector(); $pluginManager = $injector->make(PluginManager::class); $reflectedPluginManager = new \ReflectionObject($pluginManager); $injectorProp = $reflectedPluginManager->getProperty('injector'); @@ -105,44 +65,34 @@ public function testPluginManagerGetsCorrectInjector() { } public function testLoggerIsShared() { - $injector = (new DependencyGraph($this->getConfiguration()))->wireObjectGraph(); + $injector = $this->getInjector(); $this->assertSame($injector->make(LoggerInterface::class), $injector->make(LoggerInterface::class)); } public function testLoggerIsAliased() { - $injector = (new DependencyGraph($this->getConfiguration()))->wireObjectGraph(); + $injector = $this->getInjector(); $actual = $injector->make(LoggerInterface::class); - $expected = Logger::class; - $this->assertInstanceOf($expected, $actual); + $this->assertSame($this->logger, $actual); } public function testLoggerAwareObjectsHaveLoggerSet() { - $injector = (new DependencyGraph($this->getConfiguration()))->wireObjectGraph(); + $injector = $this->getInjector(); $stub = $injector->make(LoggerAwareStub::class); $this->assertSame($injector->make(LoggerInterface::class), $stub->logger); } - public function testLoggerHasConfiguredName() { - $injector = (new DependencyGraph($this->getConfiguration()))->wireObjectGraph(); - - /** @var Logger $logger */ - $logger = $injector->make(LoggerInterface::class); - - $this->assertSame('dependency-graph-test', $logger->getName()); - } - - public function testLoggerHasStreamingHandler() { - $injector = (new DependencyGraph($this->getConfiguration()))->wireObjectGraph(); - - /** @var Logger $logger */ - $logger = $injector->make(LoggerInterface::class); - - $this->assertCount(1, $logger->getHandlers()); - $this->assertInstanceOf(StreamHandler::class, $logger->getHandlers()[0]); + /** + * @return Injector + * @throws \Cspray\Labrador\Exception\DependencyInjectionException + */ + private function getInjector() : Injector { + $this->logger = new NullLogger(); + $injector = (new DependencyGraph($this->logger))->wireObjectGraph(); + return $injector; } } diff --git a/test/Integration/basic.phpt b/test/Integration/basic.phpt index 16baf8f..3869f31 100644 --- a/test/Integration/basic.phpt +++ b/test/Integration/basic.phpt @@ -5,26 +5,8 @@ Ensures basic integration works require_once dirname(dirname(__DIR__)) . '/vendor/autoload.php'; -$configuration = new class implements \Cspray\Labrador\Configuration { - public function getLogName() : string { - return 'integration-test'; - } - - public function getLogPath() : string { - return '/dev/null'; - } - - public function getInjectorProviderPath() : string { - throw new \RuntimeException('Did not expect this to be called'); - } - - public function getPlugins() : array { - throw new \RuntimeException('Did not expect this to be called'); - } -}; - -$injector = (new \Cspray\Labrador\DependencyGraph($configuration))->wireObjectGraph(); +$injector = (new \Cspray\Labrador\DependencyGraph(new \Psr\Log\NullLogger()))->wireObjectGraph(); $engine = $injector->make(\Cspray\Labrador\Engine::class); $engine->onEngineBootup(function() { diff --git a/test/Stub/LoadPluginCalledApplication.php b/test/Stub/LoadPluginCalledApplication.php index 113a1ed..259e194 100644 --- a/test/Stub/LoadPluginCalledApplication.php +++ b/test/Stub/LoadPluginCalledApplication.php @@ -17,7 +17,7 @@ public function loadPlugins(): Promise { }); } - public function execute(): Promise { + public function start(): Promise { return call(function() { $this->callOrder[] = "execute"; }); diff --git a/test/Stub/NoopApplication.php b/test/Stub/NoopApplication.php index 94b3f0e..0af8fbc 100644 --- a/test/Stub/NoopApplication.php +++ b/test/Stub/NoopApplication.php @@ -4,172 +4,11 @@ use Amp\Promise; use Amp\Success; -use Auryn\Injector; -use Cspray\Labrador\Application; -use Cspray\Labrador\AsyncEvent\Emitter; -use Cspray\Labrador\Exception\CircularDependencyException; -use Cspray\Labrador\Exception\InvalidArgumentException; -use Cspray\Labrador\Exception\InvalidStateException; -use Cspray\Labrador\Exception\NotFoundException; -use Cspray\Labrador\Plugin\Pluggable; -use Cspray\Labrador\Plugin\Plugin; -use Psr\Log\LoggerAwareTrait; -use Throwable; +use Cspray\Labrador\AbstractApplication; -class NoopApplication implements Application { +class NoopApplication extends AbstractApplication { - use LoggerAwareTrait; - - /** - * Perform whatever logic or operations your application requires; return a Promise that resolves when you app is - * finished running. - * - * This method should avoid throwing an exception and instead fail the Promise with the Exception that caused the - * application to crash. - * - * @return Promise - */ - public function execute(): Promise { - return new Success(); - } - - public function exceptionHandler(Throwable $throwable) : void { - // TODO: Implement exceptionHandler() method. - } - - /** - * Register a handler for a custom Plugin type to be invoked when loadPlugins is invoked. - * - * @param string $pluginType The fully qualified class name for the Plugin that should have the handler invoked - * @param callable $pluginHandler function(YourPluginType $plugin, ...$arguments) : Promise|Generator|void {} - * @param mixed ...$arguments Any arguments that you may pass to the handler, passed AFTER the Plugin - */ - public function registerPluginLoadHandler(string $pluginType, callable $pluginHandler, ...$arguments): void { - // TODO: Implement registerPluginLoadHandler() method. - } - - /** - * Register a handler for a custom Plugin type to be invoked when removePlugin is called with a type that matches - * the $pluginType. - * - * If plugins have not yet been loaded when the target Plugin is removed this callback will not be invoked. - * - * @param string $pluginType The fully qualified class name for the Plugin that should have the handler invoked - * @param callable $pluginHandler function(YourPluginType plugin, ...$arguments) : Promise|Generator|void {} - * @param mixed ...$arguments Any arguments that you may pass to the handler, passed AFTER the Plugin - */ - public function registerPluginRemoveHandler(string $pluginType, callable $pluginHandler, ...$arguments): void { - // TODO: Implement registerPluginRemoveHandler() method. - } - - /** - * Register a fully qualified class name that implements the Plugin interface that should be instantiated and loaded - * when loadPlugins is invoked. - * - * If a Plugin is attempted to be registered AFTER loadPlugins is invoked an IllegalStateException SHOULD be thrown - * as all registration must happen prior to loading to simplify the process. - * - * If a Plugin is attempted to be registered that does not implement the Plugin interace an IllegalArgumentException - * MUST be thrown as all registered types must implement the minimum interface. - * - * @param string $plugin The fully qualified class name of the Plugin to register - * @return void - * @throws InvalidArgumentException - * @throws InvalidStateException - */ - public function registerPlugin(string $plugin): void { - // TODO: Implement registerPlugin() method. - } - - /** - * Go through the loading and booting process for all Plugins that have been registered to this Pluggable. - * - * If Plugin A is being loaded and depends on Plugin B but Plugin B also depends on Plugin A you MUST throw a - * CircularDependencyException as the registered Plugins are invalid and cannot be properly loaded. - * - * If a PluginDependentPlugin depends on a class that does not implement the Plugin interface you MUST throw an - * InvalidStateException as a registered Plugin's state does not allow the loading of its dependencies. - * - * @return Promise Will resolve when all Plugins have completed the loading process - * @throws CircularDependencyException - */ - public function loadPlugins(): Promise { + protected function doStart() : Promise { return new Success(); } - - /** - * Removes the Plugin from the list of both registered and loaded plugins, assuming loadPlugins has been invoked. - * - * This method will also cause the Plugin to have any potential remove handlers invoked if the loading process has - * completed upon time of removal. - * - * @param string $pluginType The fully qualified class name of the Plugin to remove - * @return void - */ - public function removePlugin(string $pluginType): void { - // TODO: Implement removePlugin() method. - } - - /** - * Determine if a Plugin has been registered or not. - * - * Please note that this MAY NOT return only values that have been passed to registerPlugin. All dependencies of - * PluginDependentPlugins MUST BE implicitly registered as part of their loading process. - * - * @param string $pluginType The fully qualified class name of the Plugin to check for registry - * @return boolean True or false for whether the given plugin has been registered - */ - public function hasPluginBeenRegistered(string $pluginType): bool { - // TODO: Implement hasPluginBeenRegistered() method. - } - - /** - * @return bool True or false for whether all registered Plugins, and their dependencies, have fully loaded - */ - public function havePluginsLoaded(): bool { - // TODO: Implement havePluginsLoaded() method. - } - - /** - * Attempt to retrieve a Plugin object. - * - * If the Plugin could not be found a NotFoundException SHOULD be thrown as the state of Plugins should be known to - * the developer and a Plugin expected but not present is likely an error in configuration or Application setup and - * should be addressed immediately. - * - * If loadPlugins has not been invoked then an InvalidStateException MUST be thrown as the loading process must be - * completed before the corresponding Plugin object is available. - * - * @param string $pluginType The fully qualified class name of the Plugin to retrieve - * @return Plugin The Plugin instance used for the loading process - * @throws NotFoundException - * @throws InvalidStateException - */ - public function getLoadedPlugin(string $pluginType): Plugin { - // TODO: Implement getLoadedPlugin() method. - } - - /** - * An array of Plugin objects associated to the given Pluggable. - * - * If loadPlugins has not been invoked an InvalidStateException MUST be thrown as the loading process must be - * completed before Plugin objects are available and this is a distinct case separate from there not being any - * Plugins after the loading process making an empty array ill-suited for this error condition. - * - * @return Plugin[] - * @throws InvalidStateException - */ - public function getLoadedPlugins(): array { - // TODO: Implement getLoadedPlugins() method. - } - - /** - * An array of Plugin types that have been registered with this Pluggable, either through the registerPlugin method - * or implicitly during the loading of PluginDependentPlugins. - * - * @return string[] - */ - public function getRegisteredPlugins(): array { - // TODO: Implement getRegisteredPlugins() method. - } } diff --git a/test/_data/invalid-configuration-file-extension.txt b/test/_data/invalid-configuration-file-extension.txt deleted file mode 100644 index e69de29..0000000 diff --git a/test/_data/invalid-json-configuration.json b/test/_data/invalid-json-configuration.json deleted file mode 100644 index bc934bc..0000000 --- a/test/_data/invalid-json-configuration.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "labrador": {} -} \ No newline at end of file diff --git a/test/_data/invalid-return-type-php-configuration.php b/test/_data/invalid-return-type-php-configuration.php deleted file mode 100644 index 8aebdb2..0000000 --- a/test/_data/invalid-return-type-php-configuration.php +++ /dev/null @@ -1,3 +0,0 @@ - [] -]; diff --git a/test/_data/invalid-xml-configuration.xml b/test/_data/invalid-xml-configuration.xml deleted file mode 100644 index 90d15e9..0000000 --- a/test/_data/invalid-xml-configuration.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/test/_data/valid-array-php-configuration.php b/test/_data/valid-array-php-configuration.php deleted file mode 100644 index f771dbc..0000000 --- a/test/_data/valid-array-php-configuration.php +++ /dev/null @@ -1,17 +0,0 @@ - [ - 'environment' => 'staging', - 'applicationClass' => \Cspray\Labrador\CallbackApplication::class, - 'logging' => [ - 'name' => 'php-array-log', - 'outputPath' => 'php://stdout' - ], - 'injectorProviderPath' => "file://yadda/yadda/array", - 'plugins' => [ - \Cspray\Labrador\Test\Stub\PluginStub::class, - \Cspray\Labrador\Test\Stub\FooPluginDependentStub::class - ] - ] -]; diff --git a/test/_data/valid-configuration-instance-php-configuration.php b/test/_data/valid-configuration-instance-php-configuration.php deleted file mode 100644 index e74198c..0000000 --- a/test/_data/valid-configuration-instance-php-configuration.php +++ /dev/null @@ -1,32 +0,0 @@ - - - - xml-config-log - php://stdout - - file://yadda/yadda/xml - - Cspray\Labrador\Test\Stub\PluginStub - -