diff --git a/README.md b/README.md index 1365ea4..83217a6 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ or the [provided tools](#history). #### Disable Monitoring -You may want to disable monitoring for certain messages. There are two ways to do this: +You may want to disable monitoring for certain messages. There are several ways to do this: 1. When dispatching the message, add the `DisableMonitoringStamp`: ```php @@ -97,7 +97,7 @@ You may want to disable monitoring for certain messages. There are two ways to d $bus->dispatch(new MyMessage(), [new DisableMonitoringStamp()]) ``` -2. Add the `DisableMonitoringStamp` as a class attribute to your message: +2. Add the `DisableMonitoringStamp` as a class attribute to your message (or parent class/interface): ```php use Zenstruck\Messenger\Monitor\Stamp\DisableMonitoringStamp; @@ -106,8 +106,8 @@ You may want to disable monitoring for certain messages. There are two ways to d { } ``` -3. You may want to disable monitoring for messages that are dispatched without any handler. -You can do this by using the `DisableMonitoringStamp` with optional constructor argument `true`: + You may want to disable monitoring for messages that are dispatched without any handler. + You can do this by using the `DisableMonitoringStamp` with optional constructor argument `true`: ```php use Zenstruck\Messenger\Monitor\Stamp\DisableMonitoringStamp; @@ -116,6 +116,15 @@ You can do this by using the `DisableMonitoringStamp` with optional constructor { } ``` +3. Add the message class to the `exclude` config option (can be abstract/interface): + ```yaml + # config/packages/zenstruck_messenger_monitor.yaml + + zenstruck_messenger_monitor: + storage: + exclude: + - App\Message\MyMessage + ``` #### Description @@ -145,7 +154,7 @@ add your own in one of two ways: $bus->dispatch(new MyMessage(), [new TagStamp('tag-1'), new TagStamp('tag-2')]) ``` -2. Add the `TagStamp` as a class attribute to your message: +2. Add the `TagStamp` as a class attribute to your message (and parent class/interface): ```php use Zenstruck\Messenger\Monitor\Stamp\TagStamp; @@ -155,6 +164,8 @@ add your own in one of two ways: { } ``` + > [!TIP] + > You can also add the `TagStamp` attribute to parent classes/interfaces. #### `messenger:monitor:purge` Command @@ -300,6 +311,8 @@ when@dev: ```yaml zenstruck_messenger_monitor: storage: + # Message classes to disable monitoring for (can be abstract/interface) + exclude: [] orm: # Your Doctrine entity class that extends "Zenstruck\Messenger\Monitor\History\Model\ProcessedMessage" diff --git a/composer.json b/composer.json index f582be4..9237330 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,8 @@ "symfony/serializer": "^6.4|^7.0", "symfony/var-dumper": "^6.4|^7.0", "zenstruck/console-test": "^1.5", - "zenstruck/foundry": "^2.2" + "zenstruck/foundry": "^2.2", + "zenstruck/messenger-test": "^1.11" }, "suggest": { "knplabs/knp-time-bundle": "For human readable timestamps and durations on your dashboard.", diff --git a/config/storage_orm.php b/config/storage_orm.php index e5bbd6a..ce78c26 100644 --- a/config/storage_orm.php +++ b/config/storage_orm.php @@ -8,7 +8,9 @@ use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent; use Zenstruck\Messenger\Monitor\Command\PurgeCommand; use Zenstruck\Messenger\Monitor\Command\SchedulePurgeCommand; -use Zenstruck\Messenger\Monitor\History\HistoryListener; +use Zenstruck\Messenger\Monitor\EventListener\AddMonitorStampListener; +use Zenstruck\Messenger\Monitor\EventListener\HandleMonitorStampListener; +use Zenstruck\Messenger\Monitor\EventListener\ReceiveMonitorStampListener; use Zenstruck\Messenger\Monitor\History\ResultNormalizer; use Zenstruck\Messenger\Monitor\History\Storage; use Zenstruck\Messenger\Monitor\History\Storage\ORMStorage; @@ -29,13 +31,20 @@ ->set('.zenstruck_messenger_monitor.history.result_normalizer', ResultNormalizer::class) ->args([param('kernel.project_dir')]) - ->set('.zenstruck_messenger_monitor.history.listener', HistoryListener::class) + ->set('.zenstruck_messenger_monitor.listener.add_monitor_stamp', AddMonitorStampListener::class) + ->tag('kernel.event_listener', ['method' => '__invoke', 'event' => SendMessageToTransportsEvent::class]) + + ->set('.zenstruck_messenger_monitor.listener.receive_monitor_stamp', ReceiveMonitorStampListener::class) + ->args([ + abstract_arg('exclude_classes') + ]) + ->tag('kernel.event_listener', ['method' => '__invoke', 'event' => WorkerMessageReceivedEvent::class]) + + ->set('.zenstruck_messenger_monitor.listener.handle_monitor_stamp', HandleMonitorStampListener::class) ->args([ service('zenstruck_messenger_monitor.history.storage'), service('.zenstruck_messenger_monitor.history.result_normalizer'), ]) - ->tag('kernel.event_listener', ['method' => 'addMonitorStamp', 'event' => SendMessageToTransportsEvent::class]) - ->tag('kernel.event_listener', ['method' => 'receiveMessage', 'event' => WorkerMessageReceivedEvent::class]) ->tag('kernel.event_listener', ['method' => 'handleSuccess', 'event' => WorkerMessageHandledEvent::class]) ->tag('kernel.event_listener', ['method' => 'handleFailure', 'event' => WorkerMessageFailedEvent::class]) diff --git a/src/DependencyInjection/ZenstruckMessengerMonitorExtension.php b/src/DependencyInjection/ZenstruckMessengerMonitorExtension.php index 99e5793..c0b966e 100644 --- a/src/DependencyInjection/ZenstruckMessengerMonitorExtension.php +++ b/src/DependencyInjection/ZenstruckMessengerMonitorExtension.php @@ -35,6 +35,15 @@ public function getConfigTreeBuilder(): TreeBuilder ->children() ->arrayNode('storage') ->children() + ->arrayNode('exclude') + ->info('Message classes to disable monitoring for (can be abstract/interface)') + ->scalarPrototype() + ->validate() + ->ifTrue(fn($v) => !\class_exists($v) && !\interface_exists($v)) + ->thenInvalid('Class/interface does not exist.') + ->end() + ->end() + ->end() ->arrayNode('orm') ->children() ->scalarNode('entity_class') @@ -91,7 +100,12 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container if ($entity = $mergedConfig['storage']['orm']['entity_class'] ?? null) { $loader->load('storage_orm.php'); - $container->getDefinition('zenstruck_messenger_monitor.history.storage')->setArgument(1, $entity); + $container->getDefinition('zenstruck_messenger_monitor.history.storage') + ->setArgument(1, $entity) + ; + $container->getDefinition('.zenstruck_messenger_monitor.listener.receive_monitor_stamp') + ->setArgument(0, $mergedConfig['storage']['exclude']) + ; if (!\class_exists(Schedule::class)) { $container->removeDefinition('.zenstruck_messenger_monitor.command.schedule_purge'); diff --git a/src/EventListener/AddMonitorStampListener.php b/src/EventListener/AddMonitorStampListener.php new file mode 100644 index 0000000..fa0fd48 --- /dev/null +++ b/src/EventListener/AddMonitorStampListener.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\EventListener; + +use Symfony\Component\Messenger\Event\SendMessageToTransportsEvent; +use Zenstruck\Messenger\Monitor\Stamp\MonitorStamp; + +/** + * @author Kevin Bond + * + * @internal + */ +final class AddMonitorStampListener +{ + public function __invoke(SendMessageToTransportsEvent $event): void + { + $event->setEnvelope($event->getEnvelope()->with(new MonitorStamp())); + } +} diff --git a/src/History/HistoryListener.php b/src/EventListener/HandleMonitorStampListener.php similarity index 50% rename from src/History/HistoryListener.php rename to src/EventListener/HandleMonitorStampListener.php index f18fbdc..04fb1f2 100644 --- a/src/History/HistoryListener.php +++ b/src/EventListener/HandleMonitorStampListener.php @@ -9,60 +9,29 @@ * file that was distributed with this source code. */ -namespace Zenstruck\Messenger\Monitor\History; +namespace Zenstruck\Messenger\Monitor\EventListener; use Symfony\Component\Messenger\Envelope; -use Symfony\Component\Messenger\Event\SendMessageToTransportsEvent; use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent; -use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent; use Symfony\Component\Messenger\Exception\HandlerFailedException; use Symfony\Component\Messenger\Stamp\HandledStamp; -use Symfony\Component\Scheduler\Messenger\ScheduledStamp; -use Zenstruck\Messenger\Monitor\History\Model\Result; use Zenstruck\Messenger\Monitor\History\Model\Results; -use Zenstruck\Messenger\Monitor\Stamp\DisableMonitoringStamp; +use Zenstruck\Messenger\Monitor\History\ResultNormalizer; +use Zenstruck\Messenger\Monitor\History\Storage; use Zenstruck\Messenger\Monitor\Stamp\MonitorStamp; -use Zenstruck\Messenger\Monitor\Stamp\TagStamp; /** * @author Kevin Bond * * @internal - * - * @phpstan-import-type Structure from Result */ -final class HistoryListener +final class HandleMonitorStampListener { - public function __construct(private Storage $storage, private ResultNormalizer $normalizer) - { - } - - public function addMonitorStamp(SendMessageToTransportsEvent $event): void - { - $event->setEnvelope($event->getEnvelope()->with(new MonitorStamp())); - } - - public function receiveMessage(WorkerMessageReceivedEvent $event): void - { - $envelope = $event->getEnvelope(); - - if ($this->isMonitoringDisabled($envelope)) { - return; - } - - $stamp = $envelope->last(MonitorStamp::class); - - if (\class_exists(ScheduledStamp::class) && $scheduledStamp = $envelope->last(ScheduledStamp::class)) { - // scheduler transport doesn't trigger SendMessageToTransportsEvent - $stamp = new MonitorStamp($scheduledStamp->messageContext->triggeredAt); - - $event->addStamps(TagStamp::forSchedule($scheduledStamp)); - } - - if ($stamp instanceof MonitorStamp) { - $event->addStamps($stamp->markReceived($event->getReceiverName())); - } + public function __construct( + private Storage $storage, + private ResultNormalizer $normalizer, + ) { } public function handleSuccess(WorkerMessageHandledEvent $event): void @@ -101,35 +70,6 @@ public function handleFailure(WorkerMessageFailedEvent $event): void ); } - private function isMonitoringDisabled(Envelope $envelope): bool - { - if ($stamp = $envelope->last(DisableMonitoringStamp::class)) { - if (false === $stamp->onlyWhenNoHandler) { - return true; - } - - return $this->hasNoHandlers($envelope); - } - - $reflection = new \ReflectionClass($envelope->getMessage()); - $attributes = []; - - while (false !== $reflection && [] === $attributes) { - $attributes = $reflection->getAttributes(DisableMonitoringStamp::class); - $reflection = $reflection->getParentClass(); - } - - if ([] !== $attributes) { - if (false === $attributes[0]->newInstance()->onlyWhenNoHandler) { - return true; - } - - return $this->hasNoHandlers($envelope); - } - - return false; - } - private function createResults(Envelope $envelope, ?HandlerFailedException $exception = null): Results { $results = []; @@ -157,9 +97,4 @@ private function createResults(Envelope $envelope, ?HandlerFailedException $exce return new Results($results); } - - private function hasNoHandlers(Envelope $envelope): bool - { - return [] === $envelope->all(HandledStamp::class); - } } diff --git a/src/EventListener/ReceiveMonitorStampListener.php b/src/EventListener/ReceiveMonitorStampListener.php new file mode 100644 index 0000000..36ced8e --- /dev/null +++ b/src/EventListener/ReceiveMonitorStampListener.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\EventListener; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent; +use Symfony\Component\Messenger\Stamp\HandledStamp; +use Symfony\Component\Scheduler\Messenger\ScheduledStamp; +use Zenstruck\Messenger\Monitor\Stamp\DisableMonitoringStamp; +use Zenstruck\Messenger\Monitor\Stamp\MonitorStamp; +use Zenstruck\Messenger\Monitor\Stamp\TagStamp; + +/** + * @author Kevin Bond + * + * @internal + */ +final class ReceiveMonitorStampListener +{ + /** + * @param class-string[] $excludedClasses + */ + public function __construct(private array $excludedClasses) + { + } + + public function __invoke(WorkerMessageReceivedEvent $event): void + { + $envelope = $event->getEnvelope(); + + if ($this->isMonitoringDisabled($envelope)) { + return; + } + + $stamp = $envelope->last(MonitorStamp::class); + + if (\class_exists(ScheduledStamp::class) && $scheduledStamp = $envelope->last(ScheduledStamp::class)) { + // scheduler transport doesn't trigger SendMessageToTransportsEvent + $stamp = new MonitorStamp($scheduledStamp->messageContext->triggeredAt); + + $event->addStamps(TagStamp::forSchedule($scheduledStamp)); + } + + if ($stamp instanceof MonitorStamp) { + $event->addStamps($stamp->markReceived($event->getReceiverName())); + } + } + + private function isMonitoringDisabled(Envelope $envelope): bool + { + $messageClass = $envelope->getMessage()::class; + + foreach ($this->excludedClasses as $excludedClass) { + if (\is_a($messageClass, $excludedClass, true)) { + return true; + } + } + + if (!$stamp = DisableMonitoringStamp::firstFrom($envelope)) { + return false; + } + + if ($stamp->onlyWhenNoHandler && !$this->hasNoHandlers($envelope)) { + return false; + } + + return true; + } + + private function hasNoHandlers(Envelope $envelope): bool + { + return [] === $envelope->all(HandledStamp::class); + } +} diff --git a/src/History/Model/ProcessedMessage.php b/src/History/Model/ProcessedMessage.php index 5db7c2d..3dae513 100644 --- a/src/History/Model/ProcessedMessage.php +++ b/src/History/Model/ProcessedMessage.php @@ -44,8 +44,8 @@ abstract class ProcessedMessage private ?string $failureType = null; private ?string $failureMessage = null; - /** @var Structure[]|Results */ - private array|Results $results; + /** @var Structure[]|Results|null */ + private array|Results|null $results; public function __construct(Envelope $envelope, Results $results, ?\Throwable $exception = null) { diff --git a/src/History/Model/Results.php b/src/History/Model/Results.php index dc1abf9..085ec5b 100644 --- a/src/History/Model/Results.php +++ b/src/History/Model/Results.php @@ -26,9 +26,9 @@ final class Results implements \Countable, \IteratorAggregate, \JsonSerializable /** * @internal * - * @param Structure[] $data + * @param Structure[]|null $data */ - public function __construct(private array $data) + public function __construct(private ?array $data) { } @@ -37,7 +37,7 @@ public function __construct(private array $data) */ public function all(): array { - return $this->all ??= \array_map(static fn(array $result) => new Result($result), $this->data); + return $this->all ??= \array_map(static fn(array $result) => new Result($result), $this->data ?? []); } /** @@ -63,13 +63,13 @@ public function getIterator(): \Traversable public function count(): int { - return \count($this->data); + return \count($this->all()); } /** - * @return Structure[] + * @return Structure[]|null */ - public function jsonSerialize(): array + public function jsonSerialize(): ?array { return $this->data; } diff --git a/src/History/Model/Tags.php b/src/History/Model/Tags.php index cd5cbee..34dfa79 100644 --- a/src/History/Model/Tags.php +++ b/src/History/Model/Tags.php @@ -100,12 +100,8 @@ static function(string $tag): array { */ private static function parseFrom(Envelope $envelope): \Traversable { - foreach ((new \ReflectionClass($envelope->getMessage()))->getAttributes(TagStamp::class) as $attribute) { - yield $attribute->newInstance()->value; - } - - foreach ($envelope->all(TagStamp::class) as $tag) { - yield $tag->value; // @phpstan-ignore-line + foreach (TagStamp::from($envelope) as $tag) { + yield $tag->value; } } } diff --git a/src/Stamp/AttributeStamp.php b/src/Stamp/AttributeStamp.php new file mode 100644 index 0000000..c21b316 --- /dev/null +++ b/src/Stamp/AttributeStamp.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\Stamp; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Stamp\StampInterface; + +/** + * @author Kevin Bond + */ +abstract class AttributeStamp implements StampInterface +{ + /** + * @internal + * + * @return static[] + */ + final public static function from(Envelope $envelope): iterable + { + foreach ($envelope->all(static::class) as $stamp) { + yield $stamp; // @phpstan-ignore generator.valueType + } + + $original = $reflection = new \ReflectionClass($envelope->getMessage()); + + while (false !== $reflection) { + + foreach ($reflection->getAttributes(static::class) as $attribute) { + yield $attribute->newInstance(); + } + + $reflection = $reflection->getParentClass(); + } + + foreach ($original->getInterfaces() as $refInterface) { + foreach ($refInterface->getAttributes(static::class) as $attribute) { + yield $attribute->newInstance(); + } + } + } + + /** + * @internal + */ + final public static function firstFrom(Envelope $envelope): ?static + { + foreach (self::from($envelope) as $stamp) { + return $stamp; + } + + return null; + } +} diff --git a/src/Stamp/DisableMonitoringStamp.php b/src/Stamp/DisableMonitoringStamp.php index 1acf5e9..c7d6645 100644 --- a/src/Stamp/DisableMonitoringStamp.php +++ b/src/Stamp/DisableMonitoringStamp.php @@ -11,13 +11,11 @@ namespace Zenstruck\Messenger\Monitor\Stamp; -use Symfony\Component\Messenger\Stamp\StampInterface; - /** * @author Kevin Bond */ #[\Attribute(\Attribute::TARGET_CLASS)] -final class DisableMonitoringStamp implements StampInterface +final class DisableMonitoringStamp extends AttributeStamp { public function __construct(public readonly bool $onlyWhenNoHandler = false) { diff --git a/src/Stamp/TagStamp.php b/src/Stamp/TagStamp.php index df5b7b6..522bcb6 100644 --- a/src/Stamp/TagStamp.php +++ b/src/Stamp/TagStamp.php @@ -11,7 +11,6 @@ namespace Zenstruck\Messenger\Monitor\Stamp; -use Symfony\Component\Messenger\Stamp\StampInterface; use Symfony\Component\Scheduler\Messenger\ScheduledStamp; use Zenstruck\Messenger\Monitor\Schedule\TaskInfo; @@ -19,7 +18,7 @@ * @author Kevin Bond */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] -final class TagStamp implements StampInterface, \Stringable +final class TagStamp extends AttributeStamp implements \Stringable { public function __construct( public readonly string $value, diff --git a/tests/Feature/BundleServicesTest.php b/tests/Feature/BundleServicesTest.php new file mode 100644 index 0000000..c2edd6f --- /dev/null +++ b/tests/Feature/BundleServicesTest.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\Tests\Feature; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Messenger\Monitor\Tests\Fixture\TestService; + +/** + * @author Kevin Bond + */ +final class BundleServicesTest extends KernelTestCase +{ + /** + * @test + */ + public function autowires_services(): void + { + /** @var TestService $service */ + $service = self::getContainer()->get(TestService::class); + + $this->assertCount(1, $service->transports); + $this->assertCount(0, $service->workers); + $this->assertCount(0, $service->schedules); + } +} diff --git a/tests/Feature/HistoryTest.php b/tests/Feature/HistoryTest.php new file mode 100644 index 0000000..d8e7b32 --- /dev/null +++ b/tests/Feature/HistoryTest.php @@ -0,0 +1,168 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\Tests\Feature; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\Messenger\Exception\HandlerFailedException; +use Symfony\Component\Messenger\Exception\NoHandlerForMessageException; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Factory\ProcessedMessageFactory; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Message\MessageA; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Message\MessageAHandler; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Message\MessageB; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Message\MessageC; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Message\MessageCHandler1; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Message\MessageCHandler2; +use Zenstruck\Messenger\Test\InteractsWithMessenger; + +/** + * @author Kevin Bond + */ +final class HistoryTest extends KernelTestCase +{ + use Factories, InteractsWithMessenger, ResetDatabase; + + /** + * @test + */ + public function flow_for_single_handler(): void + { + ProcessedMessageFactory::assert()->empty(); + + $this->bus()->dispatch(new MessageA(return: 'foo')); + + $this->transport()->processOrFail(1); + + ProcessedMessageFactory::assert()->count(1); + + $message = ProcessedMessageFactory::first(); + $this->assertSame(MessageA::class, $message->type()->class()); + $this->assertSame('async', $message->transport()); + $this->assertFalse($message->isFailure()); + $this->assertCount(1, $message->results()); + $this->assertSame(['data' => 'foo'], $message->results()->all()[0]->data()); + $this->assertFalse($message->results()->all()[0]->isFailure()); + $this->assertSame(MessageAHandler::class, $message->results()->all()[0]->handler()?->class()); + $this->assertNull($message->results()->all()[0]->handler()?->description()); + } + + /** + * @test + */ + public function flow_for_single_handler_failure(): void + { + ProcessedMessageFactory::assert()->empty(); + + $this->bus()->dispatch(new MessageA(return: 'foo', throw: true)); + + $this->transport()->processOrFail(1); + + ProcessedMessageFactory::assert()->count(1); + + $message = ProcessedMessageFactory::first(); + $this->assertSame(MessageA::class, $message->type()->class()); + $this->assertTrue($message->isFailure()); + $this->assertSame(HandlerFailedException::class, $message->failure()?->class()); + $this->assertSame(\sprintf('Handling "%s" failed: error', MessageA::class), $message->failure()->description()); + $this->assertCount(1, $message->results()); + $this->assertTrue($message->results()->all()[0]->isFailure()); + $this->assertSame(\RuntimeException::class, $message->results()->all()[0]->failure()?->class()); + $this->assertSame('error', $message->results()->all()[0]->failure()->description()); + $this->assertSame(MessageAHandler::class, $message->results()->all()[0]->handler()?->class()); + $this->assertSame(['stack_trace'], \array_keys($message->results()->all()[0]->data())); + } + + /** + * @test + */ + public function flow_for_missing_handler(): void + { + ProcessedMessageFactory::assert()->empty(); + + $this->bus()->dispatch(new MessageB()); + + $this->transport()->processOrFail(1); + + ProcessedMessageFactory::assert()->count(1); + + $message = ProcessedMessageFactory::first(); + + $this->assertTrue($message->isFailure()); + $this->assertSame(NoHandlerForMessageException::class, $message->failure()?->class()); + $this->assertSame(\sprintf('No handler for message "%s".', MessageB::class), $message->failure()->description()); + $this->assertEmpty($message->results()->all()); + } + + /** + * @test + */ + public function flow_for_multiple_handlers(): void + { + ProcessedMessageFactory::assert()->empty(); + + $this->bus()->dispatch(new MessageC(return1: 'foo', return2: 'bar')); + + $this->transport()->processOrFail(1); + + ProcessedMessageFactory::assert()->count(1); + + $message = ProcessedMessageFactory::first(); + $this->assertSame(MessageC::class, $message->type()->class()); + $this->assertSame('async', $message->transport()); + $this->assertFalse($message->isFailure()); + $this->assertCount(2, $message->results()); + + $this->assertSame(['data' => 'foo'], $message->results()->all()[0]->data()); + $this->assertFalse($message->results()->all()[0]->isFailure()); + $this->assertSame(MessageCHandler1::class, $message->results()->all()[0]->handler()?->class()); + $this->assertNull($message->results()->all()[0]->handler()?->description()); + + $this->assertSame(['data' => 'bar'], $message->results()->all()[1]->data()); + $this->assertFalse($message->results()->all()[1]->isFailure()); + $this->assertSame(MessageCHandler2::class, $message->results()->all()[1]->handler()?->class()); + $this->assertSame('handle', $message->results()->all()[1]->handler()?->description()); + } + + /** + * @test + */ + public function flow_for_multiple_handlers_one_fails(): void + { + $this->bus()->dispatch(new MessageC(return1: 'foo', return2: 'bar', throw: true)); + + $this->transport()->processOrFail(1); + + ProcessedMessageFactory::assert()->count(1); + + $message = ProcessedMessageFactory::first(); + $this->assertSame(MessageC::class, $message->type()->class()); + $this->assertSame('async', $message->transport()); + $this->assertTrue($message->isFailure()); + + $this->assertSame(HandlerFailedException::class, $message->failure()?->class()); + $this->assertSame(\sprintf('Handling "%s" failed: error', MessageC::class), $message->failure()->description()); + $this->assertCount(2, $message->results()); + + $this->assertSame(['data' => 'bar'], $message->results()->all()[0]->data()); + $this->assertFalse($message->results()->all()[0]->isFailure()); + $this->assertSame(MessageCHandler2::class, $message->results()->all()[0]->handler()?->class()); + $this->assertSame('handle', $message->results()->all()[0]->handler()?->description()); + + $this->assertTrue($message->results()->all()[1]->isFailure()); + $this->assertSame(MessageCHandler1::class, $message->results()->all()[1]->handler()?->class()); + $this->assertNull($message->results()->all()[1]->handler()?->description()); + $this->assertSame(\RuntimeException::class, $message->results()->all()[1]->failure()?->class()); + $this->assertSame('error', $message->results()->all()[1]->failure()->description()); + $this->assertSame(['stack_trace'], \array_keys($message->results()->all()[1]->data())); + } +} diff --git a/tests/Integration/ZenstruckMessengerMonitorBundleTest.php b/tests/Feature/SerializerTest.php similarity index 59% rename from tests/Integration/ZenstruckMessengerMonitorBundleTest.php rename to tests/Feature/SerializerTest.php index c09fc89..aefe0b7 100644 --- a/tests/Integration/ZenstruckMessengerMonitorBundleTest.php +++ b/tests/Feature/SerializerTest.php @@ -9,47 +9,19 @@ * file that was distributed with this source code. */ -namespace Zenstruck\Messenger\Monitor\Tests\Integration; +namespace Zenstruck\Messenger\Monitor\Tests\Feature; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Clock\Test\ClockSensitiveTrait; use Symfony\Component\Serializer\Serializer; -use Zenstruck\Console\Test\InteractsWithConsole; -use Zenstruck\Foundry\Test\ResetDatabase; use Zenstruck\Messenger\Monitor\Stamp\MonitorStamp; -use Zenstruck\Messenger\Monitor\Tests\Fixture\TestService; /** * @author Kevin Bond */ -final class ZenstruckMessengerMonitorBundleTest extends KernelTestCase +final class SerializerTest extends KernelTestCase { - use ClockSensitiveTrait, InteractsWithConsole, ResetDatabase; - - /** - * @test - */ - public function autowires_services(): void - { - /** @var TestService $service */ - $service = self::getContainer()->get(TestService::class); - - $this->assertCount(1, $service->transports); - $this->assertCount(0, $service->workers); - $this->assertCount(0, $service->schedules); - } - - /** - * @test - */ - public function run_messenger_monitor_command(): void - { - $this->executeConsoleCommand('messenger:monitor') - ->assertSuccessful() - ->assertOutputContains('[!] No workers running.') - ->assertOutputContains('async n/a') - ; - } + use ClockSensitiveTrait; /** * @test diff --git a/tests/Fixture/Message/MessageA.php b/tests/Fixture/Message/MessageA.php index 39caa61..8e239b5 100644 --- a/tests/Fixture/Message/MessageA.php +++ b/tests/Fixture/Message/MessageA.php @@ -16,4 +16,9 @@ */ final class MessageA { + public function __construct( + public readonly mixed $return = null, + public readonly bool $throw = false, + ) { + } } diff --git a/tests/Fixture/Message/MessageAHandler.php b/tests/Fixture/Message/MessageAHandler.php new file mode 100644 index 0000000..c6d913a --- /dev/null +++ b/tests/Fixture/Message/MessageAHandler.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\Tests\Fixture\Message; + +use Symfony\Component\Messenger\Attribute\AsMessageHandler; + +/** + * @author Kevin Bond + */ +#[AsMessageHandler] +final class MessageAHandler +{ + public function __invoke(MessageA $message): mixed + { + if ($message->throw) { + throw new \RuntimeException('error'); + } + + return $message->return; + } +} diff --git a/tests/Fixture/Message/MessageC.php b/tests/Fixture/Message/MessageC.php new file mode 100644 index 0000000..bed4830 --- /dev/null +++ b/tests/Fixture/Message/MessageC.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\Tests\Fixture\Message; + +/** + * @author Kevin Bond + */ +final class MessageC +{ + public function __construct( + public readonly mixed $return1 = null, + public readonly mixed $return2 = null, + public readonly bool $throw = false, + ) { + } +} diff --git a/tests/Fixture/Message/MessageCHandler1.php b/tests/Fixture/Message/MessageCHandler1.php new file mode 100644 index 0000000..93ab4e7 --- /dev/null +++ b/tests/Fixture/Message/MessageCHandler1.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\Tests\Fixture\Message; + +use Symfony\Component\Messenger\Attribute\AsMessageHandler; + +/** + * @author Kevin Bond + */ +#[AsMessageHandler] +final class MessageCHandler1 +{ + public function __invoke(MessageC $message): mixed + { + if ($message->throw) { + throw new \RuntimeException('error'); + } + + return $message->return1; + } +} diff --git a/tests/Fixture/Message/MessageCHandler2.php b/tests/Fixture/Message/MessageCHandler2.php new file mode 100644 index 0000000..f345a89 --- /dev/null +++ b/tests/Fixture/Message/MessageCHandler2.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\Tests\Fixture\Message; + +use Symfony\Component\Messenger\Attribute\AsMessageHandler; + +/** + * @author Kevin Bond + */ +final class MessageCHandler2 +{ + #[AsMessageHandler] + public function handle(MessageC $message): mixed + { + return $message->return2; + } +} diff --git a/tests/Fixture/TestKernel.php b/tests/Fixture/TestKernel.php index 5293e71..71ba52f 100644 --- a/tests/Fixture/TestKernel.php +++ b/tests/Fixture/TestKernel.php @@ -20,7 +20,14 @@ use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; use Zenstruck\Foundry\ZenstruckFoundryBundle; use Zenstruck\Messenger\Monitor\Tests\Fixture\Entity\ProcessedMessage; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Message\MessageA; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Message\MessageAHandler; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Message\MessageB; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Message\MessageC; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Message\MessageCHandler1; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Message\MessageCHandler2; use Zenstruck\Messenger\Monitor\ZenstruckMessengerMonitorBundle; +use Zenstruck\Messenger\Test\ZenstruckMessengerTestBundle; /** * @author Kevin Bond @@ -33,6 +40,7 @@ public function registerBundles(): iterable { yield new FrameworkBundle(); yield new DoctrineBundle(); + yield new ZenstruckMessengerTestBundle(); yield new ZenstruckFoundryBundle(); yield new ZenstruckMessengerMonitorBundle(); } @@ -47,7 +55,12 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load 'serializer' => true, 'messenger' => [ 'transports' => [ - 'async' => 'in-memory://', + 'async' => 'test://', + ], + 'routing' => [ + MessageA::class => 'async', + MessageB::class => 'async', + MessageC::class => 'async', ], ], ]); @@ -78,6 +91,9 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load ]); $c->register(TestService::class)->setAutowired(true)->setPublic(true); + $c->register(MessageAHandler::class)->setAutowired(true)->setAutoconfigured(true); + $c->register(MessageCHandler1::class)->setAutowired(true)->setAutoconfigured(true); + $c->register(MessageCHandler2::class)->setAutowired(true)->setAutoconfigured(true); } protected function configureRoutes(RoutingConfigurator $routes): void diff --git a/tests/Integration/Command/MonitorCommandTest.php b/tests/Integration/Command/MonitorCommandTest.php new file mode 100644 index 0000000..2c81945 --- /dev/null +++ b/tests/Integration/Command/MonitorCommandTest.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\Tests\Integration\Command; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Console\Test\InteractsWithConsole; +use Zenstruck\Foundry\Test\ResetDatabase; + +/** + * @author Kevin Bond + */ +final class MonitorCommandTest extends KernelTestCase +{ + use InteractsWithConsole, ResetDatabase; + + /** + * @test + */ + public function run_messenger_monitor_command(): void + { + $this->executeConsoleCommand('messenger:monitor') + ->assertSuccessful() + ->assertOutputContains('[!] No workers running.') + ->assertOutputContains('async 0 0') + ; + } +} diff --git a/tests/Integration/DependencyInjection/ZenstruckMessengerMonitorExtensionTest.php b/tests/Integration/DependencyInjection/ZenstruckMessengerMonitorExtensionTest.php index f9c0571..5745100 100644 --- a/tests/Integration/DependencyInjection/ZenstruckMessengerMonitorExtensionTest.php +++ b/tests/Integration/DependencyInjection/ZenstruckMessengerMonitorExtensionTest.php @@ -13,9 +13,13 @@ use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractExtensionTestCase; use Matthias\SymfonyDependencyInjectionTest\PhpUnit\ContainerBuilderHasAliasConstraint; +use Matthias\SymfonyDependencyInjectionTest\PhpUnit\ContainerBuilderHasServiceDefinitionConstraint; use PHPUnit\Framework\Constraint\LogicalNot; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Zenstruck\Messenger\Monitor\DependencyInjection\ZenstruckMessengerMonitorExtension; +use Zenstruck\Messenger\Monitor\EventListener\AddMonitorStampListener; +use Zenstruck\Messenger\Monitor\EventListener\HandleMonitorStampListener; +use Zenstruck\Messenger\Monitor\EventListener\ReceiveMonitorStampListener; use Zenstruck\Messenger\Monitor\History\Model\ProcessedMessage; use Zenstruck\Messenger\Monitor\History\Storage; use Zenstruck\Messenger\Monitor\History\Storage\ORMStorage; @@ -38,6 +42,9 @@ public function no_config(): void $this->assertContainerBuilderHasAlias(Transports::class, 'zenstruck_messenger_monitor.transports'); $this->assertContainerBuilderHasAlias(Workers::class, 'zenstruck_messenger_monitor.workers'); $this->assertThat($this->container, new LogicalNot(new ContainerBuilderHasAliasConstraint(Storage::class))); + $this->assertThat($this->container, new LogicalNot(new ContainerBuilderHasServiceDefinitionConstraint('.zenstruck_messenger_monitor.listener.add_monitor_stamp'))); + $this->assertThat($this->container, new LogicalNot(new ContainerBuilderHasServiceDefinitionConstraint('.zenstruck_messenger_monitor.listener.receive_monitor_stamp'))); + $this->assertThat($this->container, new LogicalNot(new ContainerBuilderHasServiceDefinitionConstraint('.zenstruck_messenger_monitor.listener.handle_monitor_stamp'))); } /** @@ -52,6 +59,35 @@ public function orm_config(): void $this->assertContainerBuilderHasService('zenstruck_messenger_monitor.history.storage', ORMStorage::class); $this->assertContainerBuilderHasServiceDefinitionWithArgument('zenstruck_messenger_monitor.history.storage', 1, ProcessedMessageImpl::class); $this->assertContainerBuilderHasAlias(Storage::class, 'zenstruck_messenger_monitor.history.storage'); + $this->assertContainerBuilderHasService('.zenstruck_messenger_monitor.listener.add_monitor_stamp', AddMonitorStampListener::class); + $this->assertContainerBuilderHasService('.zenstruck_messenger_monitor.listener.receive_monitor_stamp', ReceiveMonitorStampListener::class); + $this->assertContainerBuilderHasServiceDefinitionWithArgument('.zenstruck_messenger_monitor.listener.receive_monitor_stamp', 0, []); + $this->assertContainerBuilderHasService('.zenstruck_messenger_monitor.listener.handle_monitor_stamp', HandleMonitorStampListener::class); + } + + /** + * @test + */ + public function storage_with_excluded_classes(): void + { + $this->load(['storage' => [ + 'orm' => ['entity_class' => ProcessedMessageImpl::class], + 'exclude' => ['stdClass'], + ]]); + + $this->assertContainerBuilderHasServiceDefinitionWithArgument('.zenstruck_messenger_monitor.listener.receive_monitor_stamp', 0, [\stdClass::class]); + } + + /** + * @test + */ + public function invalid_exclude_class(): void + { + $this->expectException(InvalidConfigurationException::class); + + $this->load(['storage' => [ + 'exclude' => ['invalid'], + ]]); } /** diff --git a/tests/Unit/EventListener/AddMonitorStampListenerTest.php b/tests/Unit/EventListener/AddMonitorStampListenerTest.php new file mode 100644 index 0000000..b256bf0 --- /dev/null +++ b/tests/Unit/EventListener/AddMonitorStampListenerTest.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\Tests\Unit\EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Event\SendMessageToTransportsEvent; +use Zenstruck\Messenger\Monitor\EventListener\AddMonitorStampListener; +use Zenstruck\Messenger\Monitor\Stamp\MonitorStamp; + +/** + * @author Kevin Bond + */ +final class AddMonitorStampListenerTest extends TestCase +{ + /** + * @test + */ + public function adds_monitor_stamp(): void + { + $listener = new AddMonitorStampListener(); + $envelope = new Envelope(new \stdClass()); + $event = new SendMessageToTransportsEvent($envelope, []); + + $this->assertNull($event->getEnvelope()->last(MonitorStamp::class)); + + $listener->__invoke($event); + + $this->assertInstanceOf(MonitorStamp::class, $event->getEnvelope()->last(MonitorStamp::class)); + } +} diff --git a/tests/Unit/EventListener/HandleMonitorStampListenerTest.php b/tests/Unit/EventListener/HandleMonitorStampListenerTest.php new file mode 100644 index 0000000..8ce932e --- /dev/null +++ b/tests/Unit/EventListener/HandleMonitorStampListenerTest.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\Tests\Unit\EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; +use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent; +use Symfony\Component\Messenger\Stamp\HandledStamp; +use Zenstruck\Messenger\Monitor\EventListener\HandleMonitorStampListener; +use Zenstruck\Messenger\Monitor\History\Model\Results; +use Zenstruck\Messenger\Monitor\History\ResultNormalizer; +use Zenstruck\Messenger\Monitor\History\Storage; +use Zenstruck\Messenger\Monitor\Stamp\MonitorStamp; + +/** + * @author Kevin Bond + */ +final class HandleMonitorStampListenerTest extends TestCase +{ + /** + * @test + */ + public function handles_success(): void + { + $envelope = new Envelope(new \stdClass(), [ + (new MonitorStamp())->markReceived('foo'), + new HandledStamp('handler', 'return'), + ]); + $storage = $this->createMock(Storage::class); + $storage->expects($this->once())->method('save')->with( + $this->isInstanceOf(Envelope::class), + $this->isInstanceOf(Results::class), + ); + + $listener = new HandleMonitorStampListener($storage, new ResultNormalizer(__DIR__)); + + $listener->handleSuccess($event = new WorkerMessageHandledEvent($envelope, 'foo')); + + $this->assertTrue($event->getEnvelope()->last(MonitorStamp::class)->isFinished()); + } + + /** + * @test + */ + public function handles_success_invalid(): void + { + $storage = $this->createMock(Storage::class); + $storage->expects($this->never())->method('save'); + + $listener = new HandleMonitorStampListener($storage, new ResultNormalizer(__DIR__)); + + $listener->handleSuccess(new WorkerMessageHandledEvent(new Envelope(new \stdClass()), 'foo')); + $listener->handleSuccess(new WorkerMessageHandledEvent(new Envelope(new \stdClass(), [new MonitorStamp()]), 'foo')); + } + + /** + * @test + */ + public function handles_failure(): void + { + $envelope = new Envelope(new \stdClass(), [(new MonitorStamp())->markReceived('foo')]); + $exception = new \RuntimeException(); + $storage = $this->createMock(Storage::class); + $storage->expects($this->once())->method('save')->with( + $this->isInstanceOf(Envelope::class), + $this->isInstanceOf(Results::class), + $exception, + ); + + $listener = new HandleMonitorStampListener($storage, new ResultNormalizer(__DIR__)); + + $listener->handleFailure($event = new WorkerMessageFailedEvent($envelope, 'foo', $exception)); + + $this->assertTrue($event->getEnvelope()->last(MonitorStamp::class)->isFinished()); + } + + /** + * @test + */ + public function handles_failure_invalid(): void + { + $storage = $this->createMock(Storage::class); + $storage->expects($this->never())->method('save'); + + $listener = new HandleMonitorStampListener($storage, new ResultNormalizer(__DIR__)); + + $listener->handleFailure(new WorkerMessageFailedEvent(new Envelope(new \stdClass()), 'foo', new \RuntimeException())); + $listener->handleFailure(new WorkerMessageFailedEvent(new Envelope(new \stdClass(), [new MonitorStamp()]), 'foo', new \RuntimeException())); + } +} diff --git a/tests/Unit/History/HistoryListenerTest.php b/tests/Unit/EventListener/ReceiveMonitorStampListenerTest.php similarity index 54% rename from tests/Unit/History/HistoryListenerTest.php rename to tests/Unit/EventListener/ReceiveMonitorStampListenerTest.php index 8d471db..94ce86b 100644 --- a/tests/Unit/History/HistoryListenerTest.php +++ b/tests/Unit/EventListener/ReceiveMonitorStampListenerTest.php @@ -9,22 +9,16 @@ * file that was distributed with this source code. */ -namespace Zenstruck\Messenger\Monitor\Tests\Unit\History; +namespace Zenstruck\Messenger\Monitor\Tests\Unit\EventListener; use PHPUnit\Framework\TestCase; use Symfony\Component\Messenger\Envelope; -use Symfony\Component\Messenger\Event\SendMessageToTransportsEvent; -use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; -use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent; use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent; use Symfony\Component\Messenger\Stamp\HandledStamp; use Symfony\Component\Scheduler\Generator\MessageContext; use Symfony\Component\Scheduler\Messenger\ScheduledStamp; use Symfony\Component\Scheduler\Trigger\TriggerInterface; -use Zenstruck\Messenger\Monitor\History\HistoryListener; -use Zenstruck\Messenger\Monitor\History\Model\Results; -use Zenstruck\Messenger\Monitor\History\ResultNormalizer; -use Zenstruck\Messenger\Monitor\History\Storage; +use Zenstruck\Messenger\Monitor\EventListener\ReceiveMonitorStampListener; use Zenstruck\Messenger\Monitor\Stamp\DisableMonitoringStamp; use Zenstruck\Messenger\Monitor\Stamp\MonitorStamp; use Zenstruck\Messenger\Monitor\Stamp\TagStamp; @@ -32,36 +26,20 @@ /** * @author Kevin Bond */ -final class HistoryListenerTest extends TestCase +final class ReceiveMonitorStampListenerTest extends TestCase { - /** - * @test - */ - public function adds_monitor_stamp(): void - { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__)); - $envelope = new Envelope(new \stdClass()); - $event = new SendMessageToTransportsEvent($envelope, []); - - $this->assertNull($event->getEnvelope()->last(MonitorStamp::class)); - - $listener->addMonitorStamp($event); - - $this->assertInstanceOf(MonitorStamp::class, $event->getEnvelope()->last(MonitorStamp::class)); - } - /** * @test */ public function skips_standard_messages_without_monitor_stamp(): void { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__)); + $listener = new ReceiveMonitorStampListener([]); $envelope = new Envelope(new \stdClass()); $event = new WorkerMessageReceivedEvent($envelope, 'foo'); $this->assertEmpty($event->getEnvelope()->all(MonitorStamp::class)); - $listener->receiveMessage($event); + $listener->__invoke($event); $this->assertEmpty($event->getEnvelope()->all(MonitorStamp::class)); } @@ -71,13 +49,13 @@ public function skips_standard_messages_without_monitor_stamp(): void */ public function marks_standard_message_as_received(): void { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__)); + $listener = new ReceiveMonitorStampListener([]); $envelope = new Envelope(new \stdClass(), [new MonitorStamp()]); $event = new WorkerMessageReceivedEvent($envelope, 'foo'); $this->assertFalse($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); - $listener->receiveMessage($event); + $listener->__invoke($event); $this->assertTrue($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); $this->assertSame('foo', $event->getEnvelope()->last(MonitorStamp::class)->transport()); @@ -89,7 +67,7 @@ public function marks_standard_message_as_received(): void */ public function marks_scheduled_message_as_received(): void { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__)); + $listener = new ReceiveMonitorStampListener([]); $envelope = new Envelope(new \stdClass(), [new ScheduledStamp(new MessageContext( 'default', 'id', @@ -100,7 +78,7 @@ public function marks_scheduled_message_as_received(): void $this->assertNull($event->getEnvelope()->last(MonitorStamp::class)); - $listener->receiveMessage($event); + $listener->__invoke($event); $this->assertTrue($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); $this->assertSame('foo', $event->getEnvelope()->last(MonitorStamp::class)->transport()); @@ -111,84 +89,72 @@ public function marks_scheduled_message_as_received(): void /** * @test */ - public function handles_success(): void + public function can_disable_monitoring_with_envelope_stamp(): void { - $envelope = new Envelope(new \stdClass(), [ - (new MonitorStamp())->markReceived('foo'), - new HandledStamp('handler', 'return'), - ]); - $storage = $this->createMock(Storage::class); - $storage->expects($this->once())->method('save')->with( - $this->isInstanceOf(Envelope::class), - $this->isInstanceOf(Results::class), - ); - - $listener = new HistoryListener($storage, new ResultNormalizer(__DIR__)); + $listener = new ReceiveMonitorStampListener([]); + $envelope = new Envelope(new \stdClass(), [new MonitorStamp(), new DisableMonitoringStamp()]); + $event = new WorkerMessageReceivedEvent($envelope, 'foo'); - $listener->handleSuccess($event = new WorkerMessageHandledEvent($envelope, 'foo')); + $listener->__invoke($event); - $this->assertTrue($event->getEnvelope()->last(MonitorStamp::class)->isFinished()); + $this->assertFalse($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); } /** * @test */ - public function handles_success_invalid(): void + public function can_disable_monitoring_message_attribute(): void { - $storage = $this->createMock(Storage::class); - $storage->expects($this->never())->method('save'); + $listener = new ReceiveMonitorStampListener([]); + $envelope = new Envelope(new DisabledMonitoringMessage(), [new MonitorStamp()]); + $event = new WorkerMessageReceivedEvent($envelope, 'foo'); - $listener = new HistoryListener($storage, new ResultNormalizer(__DIR__)); + $listener->__invoke($event); - $listener->handleSuccess(new WorkerMessageHandledEvent(new Envelope(new \stdClass()), 'foo')); - $listener->handleSuccess(new WorkerMessageHandledEvent(new Envelope(new \stdClass(), [new MonitorStamp()]), 'foo')); + $this->assertFalse($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); } /** * @test */ - public function handles_failure(): void + public function can_disable_monitoring_message_interface_attribute(): void { - $envelope = new Envelope(new \stdClass(), [(new MonitorStamp())->markReceived('foo')]); - $exception = new \RuntimeException(); - $storage = $this->createMock(Storage::class); - $storage->expects($this->once())->method('save')->with( - $this->isInstanceOf(Envelope::class), - $this->isInstanceOf(Results::class), - $exception, - ); - - $listener = new HistoryListener($storage, new ResultNormalizer(__DIR__)); + $listener = new ReceiveMonitorStampListener([]); + $envelope = new Envelope(new DisableMonitoringViaInterface(), [new MonitorStamp()]); + $event = new WorkerMessageReceivedEvent($envelope, 'foo'); - $listener->handleFailure($event = new WorkerMessageFailedEvent($envelope, 'foo', $exception)); + $listener->__invoke($event); - $this->assertTrue($event->getEnvelope()->last(MonitorStamp::class)->isFinished()); + $this->assertFalse($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); } /** * @test */ - public function handles_failure_invalid(): void + public function can_disable_monitoring_message_attribute_without_handler(): void { - $storage = $this->createMock(Storage::class); - $storage->expects($this->never())->method('save'); + $listener = new ReceiveMonitorStampListener([]); + $envelope = new Envelope(new DisabledMonitoringWithoutHandlerMessage(), [new MonitorStamp()]); + $event = new WorkerMessageReceivedEvent($envelope, 'foo'); - $listener = new HistoryListener($storage, new ResultNormalizer(__DIR__)); + $listener->__invoke($event); - $listener->handleFailure(new WorkerMessageFailedEvent(new Envelope(new \stdClass()), 'foo', new \RuntimeException())); - $listener->handleFailure(new WorkerMessageFailedEvent(new Envelope(new \stdClass(), [new MonitorStamp()]), 'foo', new \RuntimeException())); + $this->assertFalse($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); } /** * @test */ - public function can_disable_monitoring_with_envelope_stamp(): void + public function can_disable_monitoring_message_without_handler(): void { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__)); - $envelope = new Envelope(new \stdClass(), [new MonitorStamp(), new DisableMonitoringStamp()]); + $listener = new ReceiveMonitorStampListener([]); + $envelope = new Envelope(new \stdClass(), [ + new MonitorStamp(), + new DisableMonitoringStamp(true), + ]); $event = new WorkerMessageReceivedEvent($envelope, 'foo'); - $listener->receiveMessage($event); + $listener->__invoke($event); $this->assertFalse($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); } @@ -196,27 +162,58 @@ public function can_disable_monitoring_with_envelope_stamp(): void /** * @test */ - public function can_disable_monitoring_message_attribute(): void + public function handle_disable_monitoring_message_attribute_with_handler(): void { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__)); - $envelope = new Envelope(new DisabledMonitoringMessage(), [new MonitorStamp()]); + $listener = new ReceiveMonitorStampListener([]); + $envelope = new Envelope( + new EnabledMonitoringWithHandlerMessage(), + [ + new MonitorStamp(), + new HandledStamp(EnabledMonitoringWithHandlerMessageHandler::class, 'result'), + ], + ); $event = new WorkerMessageReceivedEvent($envelope, 'foo'); - $listener->receiveMessage($event); + $listener->__invoke($event); - $this->assertFalse($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); + $this->assertTrue($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); } /** * @test */ - public function can_disable_monitoring_message_attribute_without_handler(): void + public function handle_disable_monitoring_message_with_handler(): void { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__)); - $envelope = new Envelope(new DisabledMonitoringWithoutHandlerMessage(), [new MonitorStamp()]); + $listener = new ReceiveMonitorStampListener([]); + $envelope = new Envelope( + new \stdClass(), + [ + new MonitorStamp(), + new DisableMonitoringStamp(true), + new HandledStamp(EnabledMonitoringWithHandlerMessageHandler::class, 'result'), + ], + ); $event = new WorkerMessageReceivedEvent($envelope, 'foo'); - $listener->receiveMessage($event); + $listener->__invoke($event); + + $this->assertTrue($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); + } + + /** + * @test + */ + public function can_disable_monitoring_message_via_config(): void + { + $listener = new ReceiveMonitorStampListener( + [ + MessageToDisableViaConfig::class, + ] + ); + $envelope = new Envelope(new MessageToDisableViaConfig(), [new MonitorStamp()]); + $event = new WorkerMessageReceivedEvent($envelope, 'foo'); + + $listener->__invoke($event); $this->assertFalse($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); } @@ -224,21 +221,19 @@ public function can_disable_monitoring_message_attribute_without_handler(): void /** * @test */ - public function handle_disable_monitoring_message_attribute_with_handler(): void + public function can_disable_extended_monitoring_message_via_config(): void { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__)); - $envelope = new Envelope( - new EnabledMonitoringWithHandlerMessage(), + $listener = new ReceiveMonitorStampListener( [ - new MonitorStamp(), - new HandledStamp(EnabledMonitoringWithHandlerMessageHandler::class, 'result'), - ], + MessageToDisableViaConfig::class, + ] ); + $envelope = new Envelope(new ExtendedMessageToDisableViaConfig(), [new MonitorStamp()]); $event = new WorkerMessageReceivedEvent($envelope, 'foo'); - $listener->receiveMessage($event); + $listener->__invoke($event); - $this->assertTrue($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); + $this->assertFalse($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); } } @@ -257,6 +252,27 @@ class EnabledMonitoringWithHandlerMessage { } +#[DisableMonitoringStamp] +interface DisableInterface +{ +} + +abstract class ParentDisableMonitoringViaInterface implements DisableInterface +{ +} + +class DisableMonitoringViaInterface extends ParentDisableMonitoringViaInterface +{ +} + +class MessageToDisableViaConfig +{ +} + +class ExtendedMessageToDisableViaConfig extends MessageToDisableViaConfig +{ +} + class EnabledMonitoringWithHandlerMessageHandler { public function __invoke(EnabledMonitoringWithHandlerMessage $message): void diff --git a/tests/Unit/History/Model/ResultsTest.php b/tests/Unit/History/Model/ResultsTest.php new file mode 100644 index 0000000..638f86b --- /dev/null +++ b/tests/Unit/History/Model/ResultsTest.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\Tests\Unit\History\Model; + +use PHPUnit\Framework\TestCase; +use Zenstruck\Messenger\Monitor\History\Model\Results; + +/** + * @author Kevin Bond + */ +final class ResultsTest extends TestCase +{ + /** + * @test + */ + public function can_create_with_null(): void + { + $results = new Results(null); + + $this->assertCount(0, $results); + $this->assertSame([], $results->all()); + $this->assertSame([], $results->successes()); + $this->assertSame([], $results->failures()); + $this->assertNull($results->jsonSerialize()); + } +} diff --git a/tests/Unit/History/Model/TagsTest.php b/tests/Unit/History/Model/TagsTest.php index 28c2779..fe998fe 100644 --- a/tests/Unit/History/Model/TagsTest.php +++ b/tests/Unit/History/Model/TagsTest.php @@ -87,7 +87,7 @@ public function create_from_envelope(): void new TagStamp('qux'), ]); - $this->assertSame(['from', 'attribute', 'bar', 'foo', 'baz', 'qux'], (new Tags($envelope))->all()); + $this->assertSame(['foo', 'bar', 'baz', 'qux', 'from', 'attribute', 'abstract', 'interface'], (new Tags($envelope))->all()); } /** @@ -104,9 +104,19 @@ public function expand(): void } } +#[TagStamp('interface')] +interface InterfaceMessage +{ +} + +#[TagStamp('abstract')] +abstract class AbstractMessage implements InterfaceMessage +{ +} + #[TagStamp('from')] #[TagStamp('attribute')] #[TagStamp('bar')] -class TestMessage +class TestMessage extends AbstractMessage { }