diff --git a/.gitignore b/.gitignore index 1bb76567..9e0203f6 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ composer.lock # PHP CS Fixer .php_cs.cache + +# Configuration file +Tests/App/config/parameters.yml diff --git a/.travis.yml b/.travis.yml index 45aac3e8..738749e5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,7 +29,7 @@ before_script: - composer install --prefer-dist --no-interaction script: - - bin/phpunit --coverage-text --debug + - bin/simple-phpunit --coverage-text notifications: email: diff --git a/Components/DB/DBConfigurableConsumer.php b/Components/DB/DBConfigurableConsumer.php index 28b7c873..b941726d 100644 --- a/Components/DB/DBConfigurableConsumer.php +++ b/Components/DB/DBConfigurableConsumer.php @@ -6,6 +6,7 @@ use Smartbox\Integration\FrameworkBundle\Components\DB\Dbal\ConfigurableDbalProtocol; use Smartbox\Integration\FrameworkBundle\Components\DB\NoSQL\NoSQLConfigurableProtocol; use Smartbox\Integration\FrameworkBundle\Configurability\IsConfigurableService; +use Smartbox\Integration\FrameworkBundle\Core\Consumers\AbstractConsumer; use Smartbox\Integration\FrameworkBundle\Core\Consumers\ConfigurableConsumerInterface; use Smartbox\Integration\FrameworkBundle\Core\Consumers\Exceptions\NoResultsException; use Smartbox\Integration\FrameworkBundle\Core\Consumers\IsStopableConsumer; @@ -16,16 +17,20 @@ use Smartbox\Integration\FrameworkBundle\DependencyInjection\Traits\UsesSmartesbHelper; use Smartbox\Integration\FrameworkBundle\Service; -class DBConfigurableConsumer extends Service implements ConfigurableConsumerInterface +class DBConfigurableConsumer extends AbstractConsumer implements ConfigurableConsumerInterface { use IsConfigurableService; - use IsStopableConsumer; - use UsesLogger; - use UsesSmartesbHelper; /** @var ConfigurableStepsProviderInterface */ protected $configurableStepsProvider; + /** + * {@inheritdoc} + */ + protected function initialize(EndpointInterface $endpoint) + { + } + /** * @return ConfigurableStepsProviderInterface */ @@ -110,8 +115,13 @@ protected function onConsume(EndpointInterface $endpoint, MessageInterface $mess */ public function consume(EndpointInterface $endpoint) { + $sleepTime = (int) $endpoint->getOption(ConfigurableDbalProtocol::OPTION_SLEEP_TIME) * 1000; + $inactivityTrigger = (int) $endpoint->getOption(ConfigurableDbalProtocol::OPTION_INACTIVITY_TRIGGER); + $wakeup = microtime(true); + while (!$this->shouldStop()) { // Receive + $startConsumeTime = microtime(true); $message = $this->readMessage($endpoint); // Process @@ -121,12 +131,39 @@ public function consume(EndpointInterface $endpoint) $endpoint->handle($message); if ($this->logger) { - $now = \DateTime::createFromFormat('U.u', microtime(true)); - $this->logger->info('A message was consumed on '.$now->format('Y-m-d H:i:s.u')); + $this->logger->info( + 'A message was consumed on {date}', + ['date' => \DateTime::createFromFormat('U.u', microtime(true))->format('Y-m-d H:i:s.u')] + ); } - $this->onConsume($endpoint, $message); + $this->confirmMessage($endpoint, $message); + $endConsumeTime = $wakeup = microtime(true); + $this->dispatchConsumerTimingEvent((int) (($endConsumeTime - $startConsumeTime) * 1000), $message); + } + + if ((microtime(true) - $wakeup) > $inactivityTrigger) { // I did nothing since the last x seconds, so little nap... + usleep($sleepTime); } } + + $this->cleanUp($endpoint); + } + + /** + * {@inheritdoc} + */ + protected function cleanUp(EndpointInterface $endpoint) + { + //TODO: Connect to the steps provider and ask it to shut down its connection + } + + /** + * {@inheritdoc} + */ + protected function confirmMessage(EndpointInterface $endpoint, MessageInterface $message) + { + $this->onConsume($endpoint, $message); + return $message; } } diff --git a/Components/DB/Dbal/ConfigurableDbalProtocol.php b/Components/DB/Dbal/ConfigurableDbalProtocol.php index 723ebf83..ca36c31b 100644 --- a/Components/DB/Dbal/ConfigurableDbalProtocol.php +++ b/Components/DB/Dbal/ConfigurableDbalProtocol.php @@ -11,6 +11,8 @@ class ConfigurableDbalProtocol extends Protocol implements DescriptableInterface const OPTION_METHOD = 'method'; const OPTION_STOP_ON_NO_RESULTS = 'stop_on_no_results'; const OPTION_DB_CONNECTION_NAME = 'db_connection_name'; + const OPTION_SLEEP_TIME = 'sleep_time_ms'; + const OPTION_INACTIVITY_TRIGGER = 'inactivity_trigger_sec'; /** * Get static default options. @@ -23,6 +25,8 @@ public function getOptionsDescriptions() self::OPTION_METHOD => ['Method to be executed in the consumer/producer', []], self::OPTION_STOP_ON_NO_RESULTS => ['Consumer should stop on when all the records have been processed.', []], self::OPTION_DB_CONNECTION_NAME => ['Option to chose which DB connection the consumer/producer should use', []], + self::OPTION_SLEEP_TIME => ['Duration of the pause made in the consume loop, when nothing to do (slow mode), in milliseconds.', []], + self::OPTION_INACTIVITY_TRIGGER => ['Inactivity duration before switching to slow mode, in seconds.', []], ]); } @@ -30,8 +34,6 @@ public function getOptionsDescriptions() * With this method this class can configure an OptionsResolver that will be used to validate the options. * * @param OptionsResolver $resolver - * - * @return mixed */ public function configureOptionsResolver(OptionsResolver $resolver) { @@ -41,11 +43,15 @@ public function configureOptionsResolver(OptionsResolver $resolver) $resolver->setDefaults([ self::OPTION_STOP_ON_NO_RESULTS => false, self::OPTION_DB_CONNECTION_NAME => '', + self::OPTION_SLEEP_TIME => 100, + self::OPTION_INACTIVITY_TRIGGER => 10, ]); $resolver->setAllowedTypes(self::OPTION_METHOD, ['string']); $resolver->setAllowedTypes(self::OPTION_STOP_ON_NO_RESULTS, ['bool']); - $resolver->setAllowedTypes(self::OPTION_DB_CONNECTION_NAME, ['string']); + $resolver->setAllowedTypes(self::OPTION_DB_CONNECTION_NAME, 'string'); + $resolver->setAllowedTypes(self::OPTION_SLEEP_TIME, 'numeric'); + $resolver->setAllowedTypes(self::OPTION_INACTIVITY_TRIGGER, 'numeric'); } /** diff --git a/Components/Queues/Drivers/ArrayQueueDriver.php b/Components/Queues/Drivers/ArrayQueueDriver.php index e637af1c..51dda4b9 100644 --- a/Components/Queues/Drivers/ArrayQueueDriver.php +++ b/Components/Queues/Drivers/ArrayQueueDriver.php @@ -18,6 +18,14 @@ class ArrayQueueDriver extends Service implements QueueDriverInterface protected $subscribedQueue = false; protected $unacknowledgedFrame = null; + /** + * @return int + */ + public function getDequeueingTimeMs() + { + return 0; + } + /** * @return array */ diff --git a/Components/Queues/Drivers/QueueDriverInterface.php b/Components/Queues/Drivers/QueueDriverInterface.php index c6f33b06..00de3e1b 100644 --- a/Components/Queues/Drivers/QueueDriverInterface.php +++ b/Components/Queues/Drivers/QueueDriverInterface.php @@ -76,10 +76,9 @@ public function ack(); */ public function nack(); - /** * @param QueueMessageInterface $message - * @param string|null $destination + * @param string|null $destination * * @return bool */ @@ -105,4 +104,9 @@ public function createQueueMessage(); * Clean all the opened resources, must be called just before terminating the current request. */ public function doDestroy(); + + /** + * @return int The time it took in ms to de-queue and deserialize the message + */ + public function getDequeueingTimeMs(); } diff --git a/Components/Queues/Drivers/StompQueueDriver.php b/Components/Queues/Drivers/StompQueueDriver.php index a0810843..68abba13 100644 --- a/Components/Queues/Drivers/StompQueueDriver.php +++ b/Components/Queues/Drivers/StompQueueDriver.php @@ -55,6 +55,11 @@ class StompQueueDriver extends Service implements QueueDriverInterface protected $subscriptionId = false; + /** + * @var int The time it took in ms to deserialize the message + */ + protected $dequeueingTimeMs = 0; + /** * @return bool */ @@ -151,8 +156,16 @@ public function setFormat($format) $this->format = $format; } + /** + * @return int + */ + public function getDequeueingTimeMs() + { + return $this->dequeueingTimeMs; + } + /** {@inheritdoc} */ - public function configure($host, $username, $password, $format = QueueDriverInterface::FORMAT_JSON, $version = self::STOMP_VERSION, $vhost = null, $timeout = 3, $sync = true) + public function configure($host, $username, $password, $format = QueueDriverInterface::FORMAT_JSON, $version = self::STOMP_VERSION, $vhost = null, $timeout = 3, $sync = true) { $this->format = $format; $this->host = $host; @@ -269,6 +282,8 @@ public function receive() ); } + $this->dequeueingTimeMs = 0; + $this->currentFrame = $this->statefulStomp->read(); while ($this->currentFrame && !$this->isFrameFromSubscription($this->currentFrame)) { @@ -278,6 +293,7 @@ public function receive() $msg = null; if ($this->currentFrame) { + $start = microtime(true); $deserializationContext = new DeserializationContext(); if (!empty($version)) { $deserializationContext->setVersion($version); @@ -293,6 +309,9 @@ public function receive() foreach ($this->currentFrame->getHeaders() as $header => $value) { $msg->setHeader($header, $this->unescape($value)); } + + // Calculate how long it took to deserilize the message + $this->dequeueingTimeMs = (int) ((microtime(true) - $start) * 1000); } return $msg; diff --git a/Components/Queues/QueueConsumer.php b/Components/Queues/QueueConsumer.php index 2150a893..c6e67a76 100644 --- a/Components/Queues/QueueConsumer.php +++ b/Components/Queues/QueueConsumer.php @@ -14,6 +14,11 @@ */ class QueueConsumer extends AbstractConsumer implements ConsumerInterface { + /** + * @var int The time it took in ms to deserialize the message + */ + protected $dequeueingTimeMs = 0; + /** * {@inheritdoc} */ @@ -62,6 +67,7 @@ protected function cleanUp(EndpointInterface $endpoint) protected function readMessage(EndpointInterface $endpoint) { $driver = $this->getQueueDriver($endpoint); + $this->dequeueingTimeMs = $driver->getDequeueingTimeMs(); return $driver->receive(); } @@ -88,4 +94,18 @@ protected function confirmMessage(EndpointInterface $endpoint, MessageInterface $driver = $this->getQueueDriver($endpoint); $driver->ack(); } + + /** + * {@inheritdoc} + * + * @param $intervalMs int the timing interval that we would like to emanate + * + * @return mixed + */ + protected function dispatchConsumerTimingEvent($intervalMs, MessageInterface $message) + { + $intervalMs = $intervalMs + $this->dequeueingTimeMs; + + parent::dispatchConsumerTimingEvent($intervalMs, $message); + } } diff --git a/Core/Consumers/AbstractConsumer.php b/Core/Consumers/AbstractConsumer.php index 58a8f622..f844d577 100644 --- a/Core/Consumers/AbstractConsumer.php +++ b/Core/Consumers/AbstractConsumer.php @@ -6,6 +6,7 @@ use Smartbox\Integration\FrameworkBundle\Core\Messages\MessageInterface; use Smartbox\Integration\FrameworkBundle\DependencyInjection\Traits\UsesLogger; use Smartbox\Integration\FrameworkBundle\DependencyInjection\Traits\UsesSmartesbHelper; +use Smartbox\Integration\FrameworkBundle\Events\TimingEvent; use Smartbox\Integration\FrameworkBundle\Service; /** @@ -89,6 +90,8 @@ public function consume(EndpointInterface $endpoint) // Process if ($message) { + $startConsumeTime = microtime(true); + --$this->expirationCount; $this->process($endpoint, $message); @@ -102,6 +105,13 @@ public function consume(EndpointInterface $endpoint) } $this->confirmMessage($endpoint, $message); + + $endConsumeTime = microtime(true); + $this->dispatchConsumerTimingEvent((int) (($endConsumeTime - $startConsumeTime) * 1000), $message); + + if ($this->logger) { + $this->logger->debug('This message was processed in '.round(($endConsumeTime - $startConsumeTime) * 1000, 0).' ms.'); + } } } catch (\Exception $ex) { if (!$this->stop) { @@ -121,4 +131,23 @@ public function getName() return basename($name, 'Consumer'); } + + /** + * This function dispatchs a timing event with the amount of time it took to consume a message. + * + * @param $intervalMs int the timing interval that we would like to emanate + * @param MessageInterface $message + * + * @return mixed + */ + protected function dispatchConsumerTimingEvent($intervalMs, MessageInterface $message) + { + $event = new TimingEvent(TimingEvent::CONSUMER_TIMING); + $event->setIntervalMs($intervalMs); + $event->setMessage($message); + + if (null !== ($dispatcher = $this->getEventDispatcher())) { + $dispatcher->dispatch(TimingEvent::CONSUMER_TIMING, $event); + } + } } diff --git a/Core/Processors/Processor.php b/Core/Processors/Processor.php index e05631cd..7cf785a9 100644 --- a/Core/Processors/Processor.php +++ b/Core/Processors/Processor.php @@ -21,7 +21,6 @@ abstract class Processor extends Service implements ProcessorInterface const CONTEXT_PROCESSOR_DESCRIPTION = 'processor_description'; use UsesValidator; - use UsesEventDispatcher; /** * @var string diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 2725cae4..d69af4b5 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -216,6 +216,11 @@ public function addProducersNode() ->isRequired() ->end() + ->arrayNode('flows') + ->info('List of flows that are using this method') + ->prototype('variable')->end() + ->end() + ->arrayNode(ConfigurableProducerInterface::CONF_STEPS) ->info('This are the steps to execute as part of this method') ->prototype('variable')->end() diff --git a/Events/TimingEvent.php b/Events/TimingEvent.php new file mode 100644 index 00000000..19aff9cc --- /dev/null +++ b/Events/TimingEvent.php @@ -0,0 +1,50 @@ +intervalMs; + } + + /** + * @param int $intervalMs + */ + public function setIntervalMs($intervalMs) + { + $this->intervalMs = $intervalMs; + } + + /** + * @return \Smartbox\Integration\FrameworkBundle\Core\Messages\MessageInterface + */ + public function getMessage() + { + return $this->message; + } + + /** + * @param \Smartbox\Integration\FrameworkBundle\Core\Messages\MessageInterface $message + */ + public function setMessage($message) + { + $this->message = $message; + } +} diff --git a/Resources/config/consumers.yml b/Resources/config/consumers.yml index ad1ecdb1..f057133a 100644 --- a/Resources/config/consumers.yml +++ b/Resources/config/consumers.yml @@ -6,4 +6,5 @@ services: class: %smartesb.consumers.queue.class% calls: - [ setId, ['smartesb.consumers.queue']] - - [ setSmartesbHelper, [@smartesb.helper]] + - [ setSmartesbHelper, ['@smartesb.helper']] + - [ setEventDispatcher, ['@event_dispatcher']] diff --git a/Resources/config/default_endpoint_routes.yml b/Resources/config/default_endpoint_routes.yml index 87a08cbe..2cd0ae99 100644 --- a/Resources/config/default_endpoint_routes.yml +++ b/Resources/config/default_endpoint_routes.yml @@ -1,29 +1,29 @@ queues.generic: - pattern: "queue://{queue_driver}/{queue}" + path: "queue://{queue_driver}/{queue}" defaults: - _protocol: @smartesb.protocols.queue + _protocol: "@smartesb.protocols.queue" prefix: "%kernel.environment%" requirements: queue: "[a-zA-Z0-9/]+" queue_driver: "[a-zA-Z0-9]+" direct: - pattern: "direct://{path}" + path: "direct://{path}" defaults: - _protocol: @smartesb.protocols.direct + _protocol: "@smartesb.protocols.direct" requirements: path: "[a-zA-Z0-9/_]+" service: - pattern: "service://{service}/{method}" + path: "service://{service}/{method}" defaults: - _protocol: @smartesb.protocols.service + _protocol: "@smartesb.protocols.service" requirements: service: "[a-zA-Z0-9/_.-]+" method: "[a-zA-Z0-9_]+" csv.generic: - pattern: "csv://generic/{path}" + path: "csv://generic/{path}" defaults: _protocol: "@smartesb.protocols.configurable.csv_file" _consumer: "@smartesb.consumers.generic_csv" diff --git a/Resources/config/events_deferring.yml b/Resources/config/events_deferring.yml index d20d5db3..57e6ee36 100644 --- a/Resources/config/events_deferring.yml +++ b/Resources/config/events_deferring.yml @@ -7,7 +7,7 @@ services: class: %smartesb.handlers.events_deferring.class% calls: - [setId, ['smartesb.handlers.events']] - - [setEventDispatcher, [@event_dispatcher]] + - [setEventDispatcher, ['@event_dispatcher']] - [setFlowsVersion, [%smartesb.flows_version%]] smartesb.registry.event_filters: diff --git a/Resources/config/producers.yml b/Resources/config/producers.yml index 5d2c993b..f9c6049e 100644 --- a/Resources/config/producers.yml +++ b/Resources/config/producers.yml @@ -12,26 +12,26 @@ services: class: %smartesb.producers.direct.class% calls: - [setId, ['smartesb.producer.direct']] - - [setItineraryResolver, [@smartesb.itineray_resolver]] + - [setItineraryResolver, ['@smartesb.itineray_resolver']] smartesb.producers.json_file: class: %smartesb.producers.json_file.class% calls: - [setId, ['smartesb.producers.json_file']] - - [setSerializer, [@serializer]] + - [setSerializer, ['@serializer']] # STOMP smartesb.producers.queue: class: %smartesb.producers.queue.class% calls: - [setId, ['smartesb.producers.queue']] - - [setSerializer, [@serializer]] - - [setDriverRegistry, [@smartesb.drivers.queue._registry]] + - [setSerializer, ['@serializer']] + - [setDriverRegistry, ['@smartesb.drivers.queue._registry']] # NoSQL smartesb.producers.service: class: %smartesb.producers.service.class% calls: - [setId, ['smartesb.producers.service']] - - [setContainer, [@service_container]] + - [setContainer, ['@service_container']] diff --git a/Resources/config/protocols.yml b/Resources/config/protocols.yml index 441a38b1..857a98fe 100644 --- a/Resources/config/protocols.yml +++ b/Resources/config/protocols.yml @@ -23,35 +23,35 @@ services: smartesb.protocols.direct: class: %smartesb.protocols.direct.class% calls: - - [setDefaultProducer, [@smartesb.producer.direct]] - - [setDefaultHandler, [@smartesb.handlers.sync]] + - [setDefaultProducer, ['@smartesb.producer.direct']] + - [setDefaultHandler, ['@smartesb.handlers.sync']] # JSON FILE smartesb.protocols.json_file: class: %smartesb.protocols.json_file.class% calls: - - [setDefaultProducer, [@smartesb.producers.json_file]] + - [setDefaultProducer, ['@smartesb.producers.json_file']] # Queues smartesb.protocols.service: class: %smartesb.protocols.service.class% calls: - - [setDefaultProducer, [@smartesb.producers.service]] - - [setDefaultHandler, [@smartesb.handlers.async]] + - [setDefaultProducer, ['@smartesb.producers.service']] + - [setDefaultHandler, ['@smartesb.handlers.async']] # Queues smartesb.protocols.queue: class: %smartesb.protocols.queue.class% calls: - - [setDefaultProducer, [@smartesb.producers.queue]] - - [setDefaultConsumer, [@smartesb.consumers.queue]] - - [setDefaultHandler, [@smartesb.handlers.async]] + - [setDefaultProducer, ['@smartesb.producers.queue']] + - [setDefaultConsumer, ['@smartesb.consumers.queue']] + - [setDefaultHandler, ['@smartesb.handlers.async']] #NoSQL smartesb.protocols.configurable.nosql: class: %smartesb.protocols.configurable.nosql.class% calls: - - [setDefaultHandler, [@smartesb.handlers.async]] + - [setDefaultHandler, ['@smartesb.handlers.async']] smartesb.protocols.configurable.webservice: class: %smartesb.protocols.configurable.webservice.class% @@ -65,10 +65,10 @@ services: smartesb.protocols.configurable.dbal: class: %smartesb.protocols.configurable.dbal.class% calls: - - [setDefaultHandler, [@smartesb.handlers.async]] + - [setDefaultHandler, ['@smartesb.handlers.async']] # Csv File smartesb.protocols.configurable.csv_file: class: "%smartesb.protocols.csv_file.class%" calls: - - [setDefaultHandler, [@smartesb.handlers.async]] + - [setDefaultHandler, ['@smartesb.handlers.async']] diff --git a/Resources/config/routing.yml b/Resources/config/routing.yml index c59debbe..23de8b4e 100644 --- a/Resources/config/routing.yml +++ b/Resources/config/routing.yml @@ -36,8 +36,8 @@ services: smartesb.router.itineraries: class: %smartesb.internal_router.class% arguments: - - @service_container - - @@SmartboxIntegrationFrameworkBundle/Resources/config/default_routing_itineraries.yml + - '@service_container' + - '@@SmartboxIntegrationFrameworkBundle/Resources/config/default_routing_itineraries.yml' - %smartesb.router.itineraries.options% tags: - { name: monolog.logger, channel: "router.itineraries" } @@ -45,7 +45,7 @@ services: smartesb.router.endpoints: class: %smartesb.internal_router.class% arguments: - - @service_container + - '@service_container' - "%kernel.root_dir%/config/routing_endpoints_%kernel.environment%.yml" - %smartesb.router.endpoints.options% tags: @@ -54,14 +54,14 @@ services: smartesb.router.itineraries.cache_warmer: class: %router.cache_warmer.class% arguments: - - @smartesb.router.itineraries + - '@smartesb.router.itineraries' tags: - { name: "kernel.cache_warmer" } smartesb.router.endpoints.cache_warmer: class: %router.cache_warmer.class% arguments: - - @smartesb.router.endpoints + - '@smartesb.router.endpoints' tags: - { name: "kernel.cache_warmer" } @@ -71,6 +71,6 @@ services: smartesb.routing.itineraries_routes_loader: class: Smartbox\Integration\FrameworkBundle\Configurability\Routing\ItinerariesRoutesLoader calls: - - [ setContainer, [ @service_container ] ] + - [ setContainer, [ '@service_container' ] ] tags: - { name: routing.loader } diff --git a/Tests/App/config/parameters.yml b/Tests/App/config/parameters.yml.dist similarity index 100% rename from Tests/App/config/parameters.yml rename to Tests/App/config/parameters.yml.dist diff --git a/Tests/Unit/Components/DB/DBConfigurableConsumerTest.php b/Tests/Unit/Components/DB/DBConfigurableConsumerTest.php new file mode 100644 index 00000000..bfc99b06 --- /dev/null +++ b/Tests/Unit/Components/DB/DBConfigurableConsumerTest.php @@ -0,0 +1,98 @@ +messageFactory = $this->createMock(MessageFactory::class); + $this->stepProvider = $this->createMock(ConfigurableStepsProviderInterface::class); + $this->helper = $this->createMock(SmartesbHelper::class); + + $this->consumer = new DBConfigurableConsumer(); + $this->consumer->setConfHelper(new ConfigurableServiceHelper()); + $this->consumer->setConfigurableStepsProvider($this->stepProvider); + $this->consumer->setMessageFactory($this->messageFactory); + $this->consumer->setSmartesbHelper($this->helper); + $this->consumer->setExpirationCount(2); + $this->consumer->setMethodsConfiguration([ + 'myMethod' => [ + ConfigurableConsumerInterface::CONFIG_ON_CONSUME => [], + ConfigurableConsumerInterface::CONFIG_QUERY_STEPS => [], + ConfigurableConsumerInterface::CONFIG_QUERY_RESULT => $this->createMock(SerializableInterface::class), + ], + ]); + + $this->helper->expects($this->any())->method('getMessageFactory')->willReturn($this->messageFactory); + } + + public function testGetConfigurableStepsProvider() + { + $this->assertSame($this->stepProvider, $this->consumer->getConfigurableStepsProvider()); + } + + /** + * @group time-sensitive + */ + public function testConsume() + { + $options = [ + ConfigurableDbalProtocol::OPTION_METHOD => 'myMethod', + ConfigurableDbalProtocol::OPTION_STOP_ON_NO_RESULTS => true, + ConfigurableDbalProtocol::OPTION_SLEEP_TIME => 10000, + ConfigurableDbalProtocol::OPTION_INACTIVITY_TRIGGER => 1, + ]; + + /** @var EndpointInterface|\PHPUnit_Framework_MockObject_MockObject $endpoint */ + $endpoint = $this->createMock(EndpointInterface::class); + $endpoint->expects($this->any())->method('getOptions')->willReturn($options); + $endpoint->expects($this->any()) + ->method('getOption') + ->willReturnCallback(function ($key) use ($options) { + return $options[$key]; + }); + + $this->messageFactory->expects($this->exactly(2)) + ->method('createMessage') + ->willReturn($this->createMock(MessageInterface::class)); + + $start = microtime(true); + $this->consumer->consume($endpoint); + $this->assertLessThan(10, microtime(true) - $start, 'Execution should no last more than 10s'); + } +} diff --git a/Tests/Unit/Components/DB/Dbal/ConfigurableDbalProtocolTest.php b/Tests/Unit/Components/DB/Dbal/ConfigurableDbalProtocolTest.php index 0b899ad2..355de8b8 100644 --- a/Tests/Unit/Components/DB/Dbal/ConfigurableDbalProtocolTest.php +++ b/Tests/Unit/Components/DB/Dbal/ConfigurableDbalProtocolTest.php @@ -10,8 +10,14 @@ */ class ConfigurableDbalProtocolTest extends \PHPUnit_Framework_TestCase { + /** + * @var ConfigurableDbalProtocol + */ private $dbalProtocol; + /** + * @var array + */ private $expectedOptions; protected function setUp() @@ -21,6 +27,8 @@ protected function setUp() ConfigurableDbalProtocol::OPTION_METHOD, ConfigurableDbalProtocol::OPTION_STOP_ON_NO_RESULTS, ConfigurableDbalProtocol::OPTION_DB_CONNECTION_NAME, + ConfigurableDbalProtocol::OPTION_SLEEP_TIME, + ConfigurableDbalProtocol::OPTION_INACTIVITY_TRIGGER, ]; } diff --git a/Tests/Unit/Consumer/AbstractConsumerTest.php b/Tests/Unit/Consumer/AbstractConsumerTest.php new file mode 100644 index 00000000..b648fc6c --- /dev/null +++ b/Tests/Unit/Consumer/AbstractConsumerTest.php @@ -0,0 +1,82 @@ +getMockForAbstractClass(AbstractConsumer::class, + ['initialize', 'readMessage'], + '', + true, + true, + true, + ['shouldStop'] + ); + + $consumer + ->expects($this->once()) + ->method('initialize') + ; + + $consumer + ->expects($this->exactly(2)) + ->method('shouldStop') + ->willReturnOnConsecutiveCalls(false, true); + + $consumer + ->expects($this->once()) + ->method('readMessage') + ->willReturn(new Message()); + + $eventDispatcher = $this->getMockForAbstractClass(EventDispatcherInterface::class); + + $eventDispatcher->expects($this->once()) + ->method('dispatch') + ->with($this->equalTo('smartesb.consumer.timing'), $this->callback(function ($event) use ($lowerBoundMs, $upperBoundMs) { + $this->assertInstanceOf(TimingEvent::class, $event); + $interval = $event->getIntervalMs(); + + //here we need to allow for a small variance in the amount of time taken to 'handle' the message + return $interval > $lowerBoundMs && $interval < $upperBoundMs; + })) + ; + + $consumer->setEventDispatcher($eventDispatcher); + + $endpoint = $this->createMock(Endpoint::class); + $endpoint->expects($this->once()) + ->method('handle') + ->will( + $this->returnCallback(function () use ($handleTimeUs) { + usleep($handleTimeUs); + }) + ) + ; + + $consumer->consume($endpoint); + } + + + public function testDoesNotFailWhenNoDispatcher() + { + $consumer = $this->getMockForAbstractClass(AbstractConsumer::class); + + $class = new \ReflectionClass($consumer); + $method = $class->getMethod('dispatchConsumerTimingEvent'); + $method->setAccessible(true); + + $method->invokeArgs($consumer, [1, new Message()]); + } +} diff --git a/Tools/MockClients/FakeRestClient.php b/Tools/MockClients/FakeRestClient.php index c611162e..350c0f04 100644 --- a/Tools/MockClients/FakeRestClient.php +++ b/Tools/MockClients/FakeRestClient.php @@ -23,23 +23,94 @@ public function send(RequestInterface $request, array $options = []) $this->checkInitialisation(); $this->actionName = $this->prepareActionName($request->getMethod(), $request->getUri()); - if (getenv('MOCKS_ENABLED') === 'true') { + $mocksEnabled = getenv('MOCKS_ENABLED'); + $displayRequest = getenv('DISPLAY_REQUEST'); + $recordResponse = getenv('RECORD_RESPONSE'); + + if ('true' === $mocksEnabled) { + $mocksMessage = 'MOCKS/'; + } else { + $mocksMessage = ''; + } + + if ('true' === $displayRequest) { + $requestUri = $request->getUri()->getScheme().'://'.$request->getUri()->getAuthority().$request->getUri()->getPath(); + if ($request->getUri()->getQuery()) { + $requestUri .= '?'.$request->getUri()->getQuery(); + } + $requestMethod = $request->getMethod(); + + $requestBody = ''; + if (isset($options['body'])) { + $requestBody = $options['body']; + } + + $requestHeaders = []; + if (isset($options['headers']) && is_array($options['headers'])) { + foreach ($options['headers'] as $headerName => $headerValue) { + $requestHeaders[] = $headerName.' => '.$headerValue; + } + } + + $requestQuery = []; + if (isset($options['query']) && is_array($options['query'])) { + foreach ($options['query'] as $queryName => $queryValue) { + $requestQuery[] = $queryName.' => '.$queryValue; + } + } + + echo "\nREQUEST (".$mocksMessage.'REST) for '.$requestUri.' / '.$requestMethod; + echo "\n====================================================================================================="; + echo "\nHEADERS:\n".implode($requestHeaders, " \n"); + echo "\nQUERY:\n".implode($requestQuery, " \n"); + echo "\nRAW BODY:\n".$requestBody; + echo "\nPRETTY SEXY BODY:\n".$this->prettyJson($requestBody); + echo "\n====================================================================================================="; + echo "\n\n"; + } + + if ('true' === $mocksEnabled) { try { - return $this->getResponseFromCache($this->actionName, self::CACHE_SUFFIX); + $response = $this->getResponseFromCache($this->actionName, self::CACHE_SUFFIX); } catch (\InvalidArgumentException $e) { throw $e; } + } else { + $response = parent::send($request, $options); } - $response = parent::send($request, $options); + if ('true' === $displayRequest) { + $content = $this->getResponseContent($response); + $body = $this->getBodyContent($response); + echo "\nRESPONSE (".$mocksMessage.'REST) for '.$requestUri.' / '.$requestMethod; + echo "\n====================================================================================================="; + echo "\nRAW RESPONSE:\n".$content; + echo "\nPRETTY SEXY RESPONSE:\n".$this->prettyJson($content); + echo "\nPRETTY SEXY BODY:\n".$this->prettyJson($body); + echo "\n====================================================================================================="; + echo "\n\n"; + } - if (getenv('RECORD_RESPONSE') === 'true') { + if ('true' === $recordResponse) { $this->setResponseInCache($this->actionName, $response, self::CACHE_SUFFIX); } return $response; } + /** Return a much nicer json string. + * + * @param $uglyJson + * + * @return string + */ + private function prettyJson($uglyJson) + { + $json = json_decode($uglyJson); + + return json_encode($json, JSON_PRETTY_PRINT); + } + /** * {@inheritdoc} */ @@ -48,7 +119,7 @@ public function request($method, $uri = null, array $options = []) $this->checkInitialisation(); $this->actionName = $this->prepareActionName($method, $uri); - if (getenv('MOCKS_ENABLED') === 'true') { + if ('true' === getenv('MOCKS_ENABLED')) { try { return $this->getResponseFromCache($this->actionName, self::CACHE_SUFFIX); } catch (\InvalidArgumentException $e) { @@ -58,7 +129,7 @@ public function request($method, $uri = null, array $options = []) $response = parent::request($method, $uri, $options); - if (getenv('RECORD_RESPONSE') === 'true') { + if ('true' === getenv('RECORD_RESPONSE')) { $this->setResponseInCache($this->actionName, $response, self::CACHE_SUFFIX); } @@ -83,9 +154,31 @@ protected function getResponseFromCache($resource, $suffix = null) return new Psr7\Response($response['status'], $response['headers'], $response['body'], $response['version'], $response['reason']); } + /** + * @param $resource + * @param Psr7\Response $response + * @param null $suffix + */ protected function setResponseInCache($resource, $response, $suffix = null) { - /* @var Psr7\Response $response */ + $rawRecordedResponse = getenv('RAW_RECORDED_RESPONSE'); + + $content = $this->getResponseContent($response); + + if ('true' !== $rawRecordedResponse) { // By default, we record a pretty response. + $content = $this->prettyJson($content); + } + + $this->trait_setResponseInCache($resource, $content, $suffix); + } + + /** + * @param Psr7\Response $response + * + * @return string + */ + protected function getResponseContent($response) + { $content = json_encode( [ 'status' => $response->getStatusCode(), @@ -95,9 +188,21 @@ protected function setResponseInCache($resource, $response, $suffix = null) 'reason' => $response->getReasonPhrase(), ] ); + $response->getBody()->rewind(); + + return $content; + } + /** + * @param Psr7\Response $response + * + * @return string + */ + protected function getBodyContent($response) + { + $body = $response->getBody()->getContents(); $response->getBody()->rewind(); - $this->trait_setResponseInCache($resource, $content, $suffix); + return $body; } } diff --git a/Tools/MockClients/FakeSoapClient.php b/Tools/MockClients/FakeSoapClient.php index 345a77ad..0ed9863b 100644 --- a/Tools/MockClients/FakeSoapClient.php +++ b/Tools/MockClients/FakeSoapClient.php @@ -18,10 +18,10 @@ public function __construct($wsdl, array $options = array()) if (isset($options['MockCacheDir'])) { $this->cacheDir = $options['MockCacheDir']; } - if (getenv('RECORD_RESPONSE') === 'true') { + if ('true' === getenv('RECORD_RESPONSE')) { $this->saveWsdlToCache($wsdl, $options); } - if (getenv('MOCKS_ENABLED') === 'true') { + if ('true' === getenv('MOCKS_ENABLED')) { $wsdl = $this->getWsdlPathFromCache($wsdl, $options); $options['resolve_wsdl_remote_includes'] = false; } @@ -59,36 +59,91 @@ public function __doRequest($request, $location, $action, $version, $oneWay = 0) $this->checkInitialisation(); $actionName = md5($location).'_'.$this->actionName; - if (getenv('MOCKS_ENABLED') === 'true') { + $mocksEnabled = getenv('MOCKS_ENABLED'); + $displayRequest = getenv('DISPLAY_REQUEST'); + $recordResponse = getenv('RECORD_RESPONSE'); + $rawRecordedResponse = getenv('RAW_RECORDED_RESPONSE'); + + if ('true' === $mocksEnabled) { + $mocksMessage = 'MOCKS/'; + } else { + $mocksMessage = ''; + } + + if ('true' === $displayRequest) { + $requestHeaders = []; + if (isset($this->requestHeaders) && is_array($this->requestHeaders)) { + foreach ($this->requestHeaders as $headerName => $headerValue) { + $requestHeaders[] = $headerName.' => '.$headerValue; + } + } + + echo "\nREQUEST (".$mocksMessage."SOAP) for $location / $action / Version $version"; + echo "\n====================================================================================================="; + echo "\nHEADERS"; + echo "\n====================================================================================================="; + echo "\n ".implode($requestHeaders, "\n "); + echo "\n====================================================================================================="; + echo "\nRAW REQUEST"; + echo "\n====================================================================================================="; + echo "\n".$request; + echo "\n====================================================================================================="; + echo "\nPRETTY SEXY REQUEST"; + echo "\n====================================================================================================="; + echo "\n".$this->prettyXML($request); + echo "\n====================================================================================================="; + echo "\n\n"; + } + + if ('true' === $mocksEnabled) { try { $response = $this->getResponseFromCache($actionName, self::CACHE_SUFFIX); $this->lastResponseCode = 200; - return $response; } catch (\InvalidArgumentException $e) { throw $e; } + } else { + $response = parent::__doRequest($request, $location, $action, $version, $oneWay); } - $response = parent::__doRequest($request, $location, $action, $version, $oneWay); - - if (getenv('DISPLAY_REQUEST') === 'true') { - echo "\nREQUEST for $location / $action / Version $version"; + if ('true' === $displayRequest) { + echo "\nRESPONSE (".$mocksMessage."SOAP) for $location / $action / Version $version"; echo "\n====================================================================================================="; - echo "\n".$request; - echo "\n====================================================================================================="; - echo "\n\n"; - echo "\nRESPONSE"; + echo "\nRAW RESPONSE"; echo "\n====================================================================================================="; echo "\n".$response; echo "\n====================================================================================================="; + echo "\nPRETTY SEXY RESPONSE"; + echo "\n====================================================================================================="; + echo "\n".$this->prettyXML($response); echo "\n====================================================================================================="; echo "\n\n"; } - if (getenv('RECORD_RESPONSE') === 'true') { - $this->setResponseInCache($actionName, $response, self::CACHE_SUFFIX); + if ('true' === $recordResponse) { + $recordedResponse = $response; + if ('true' !== $rawRecordedResponse) { // By default, we record a pretty response. + $recordedResponse = $this->prettyXML($response); + } + $this->setResponseInCache($actionName, $recordedResponse, self::CACHE_SUFFIX); } return $response; } + + /** Return a much nicer XML string. + * + * @param $uglyXML + * + * @return string + */ + private function prettyXML($uglyXML) + { + $doc = new \DOMDocument(); + $doc->preserveWhiteSpace = false; + $doc->formatOutput = true; + $doc->loadXML($uglyXML); + + return $doc->saveXML(); + } } diff --git a/composer.json b/composer.json index 76615beb..29f53f5c 100644 --- a/composer.json +++ b/composer.json @@ -27,15 +27,26 @@ "smartbox/core-bundle": "^1.0.0" }, "require-dev": { - "phpunit/php-code-coverage": "~4.0", - "phpunit/phpunit-mock-objects": "~3.2", - "phpunit/phpunit": "~5.4", + "symfony/phpunit-bridge": "*", "sensio/generator-bundle": "~2.3", "mongodb/mongodb": "~1.0" }, + "scripts": { + "post-install-cmd": [ + "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters" + ], + "post-update-cmd": [ + "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters" + ] + }, "config": { "bin-dir": "bin" }, "minimum-stability": "dev", - "prefer-stable" : true + "prefer-stable" : true, + "extra": { + "incenteev-parameters": { + "file": "Tests/App/config/parameters.yml" + } + } }