Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ CHANGELOG
* Add `framework.profiler.excluded_paths` and `framework.profiler.excluded_http_codes` to skip profiling requests matching a path or answered with a given HTTP status code
* Add `framework.property_access.wildcard_reads` option to read every element of a collection through a `[*]` path
* Instantiate on the console only the bundles that override the deprecated `Bundle::registerCommands()` method
* Add `MessengerAssertionsTrait` to `KernelTestCase`, with `assertQueuedMessageCount()`, `getQueuedMessages()`, `getMessengerTransport()` and `consumeQueuedMessages()` for in-memory Messenger transports

8.1
---
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ abstract class KernelTestCase extends TestCase
{
use ConsoleCommandAssertionsTrait;
use MailerAssertionsTrait;
use MessengerAssertionsTrait;
use NotificationAssertionsTrait;

protected static ?string $class = null;
Expand Down
192 changes: 192 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/Test/MessengerAssertionsTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\FrameworkBundle\Test;

use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent;
use Symfony\Component\Messenger\EventListener\StopWorkerOnIdleListener;
use Symfony\Component\Messenger\EventListener\StopWorkerOnMessageLimitListener;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Symfony\Component\Messenger\Transport\InMemory\InMemoryTransport;
use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface;
use Symfony\Component\Messenger\Worker;

/**
* Assertions and helpers for the messages queued on in-memory Messenger transports.
*/
trait MessengerAssertionsTrait
{
/**
* Asserts the number of messages queued on an in-memory transport.
*
* @param string|null $messageClass Counts only the messages that are instances of this class
*/
public static function assertQueuedMessageCount(int $count, string $transport, ?string $messageClass = null, string $message = ''): void
{
$envelopes = self::getQueuedMessages($transport);

if (null !== $messageClass) {
if (!class_exists($messageClass) && !interface_exists($messageClass)) {
throw new \InvalidArgumentException(\sprintf('The message class "%s" given to assertQueuedMessageCount() does not exist.', $messageClass));
}

$envelopes = array_filter($envelopes, static fn (Envelope $envelope) => $envelope->getMessage() instanceof $messageClass);
Comment thread
nicolas-grekas marked this conversation as resolved.
}

self::assertCount($count, $envelopes, $message ?: \sprintf('Failed asserting that the "%s" transport has %d queued message(s)%s.', $transport, $count, null === $messageClass ? '' : ' of class "'.$messageClass.'"'));
}

/**
* Returns the envelopes queued on an in-memory transport, delayed ones included.
*
* @return Envelope[]
*/
public static function getQueuedMessages(string $transport): array
{
return self::getMessengerTransport($transport)->all();
}

/**
* Handles the messages queued on an in-memory transport with the message bus of the application.
*
* The worker listeners of the application run as in production: a failing message is sent for
* retry or to the failure transport as configured. A message sent for retry is consumed again
* without waiting for its delay, so that the retries of a test play out within the call, and
* the failure that exhausts them is rethrown. A message the application delayed itself is left
* in the transport until it is due.
*
* @param int|null $limit Stops after this number of messages, instead of draining the queue
*
* @return int The number of messages that were handled
*/
public static function consumeQueuedMessages(string $transport, ?int $limit = null): int
{
$container = static::getContainer();
$receiver = self::createRetryAwareReceiver(self::getMessengerTransport($transport));
$bus = $container->get('messenger.routable_message_bus');
/** @var EventDispatcherInterface $dispatcher */
$dispatcher = $container->get('event_dispatcher');

$handled = 0;
$failures = [];
$countHandled = static function () use (&$handled): void {
++$handled;
};
$recordFailure = static function (WorkerMessageFailedEvent $event) use (&$failures): void {
// a failure the retry strategy replays is not the outcome of this run
if (!$event->willRetry()) {
$failures[] = $event->getThrowable();
}
};
$subscribers = [new StopWorkerOnIdleListener()];

if (null !== $limit) {
$subscribers[] = new StopWorkerOnMessageLimitListener($limit);
}

foreach ($subscribers as $subscriber) {
$dispatcher->addSubscriber($subscriber);
}
$dispatcher->addListener(WorkerMessageHandledEvent::class, $countHandled);
$dispatcher->addListener(WorkerMessageFailedEvent::class, $recordFailure);

try {
(new Worker([$transport => $receiver], $bus, $dispatcher))->run(['sleep' => 0]);
Comment thread
nicolas-grekas marked this conversation as resolved.
} finally {
foreach ($subscribers as $subscriber) {
$dispatcher->removeSubscriber($subscriber);
}
$dispatcher->removeListener(WorkerMessageHandledEvent::class, $countHandled);
$dispatcher->removeListener(WorkerMessageFailedEvent::class, $recordFailure);
}

if ($failures) {
Comment thread
nicolas-grekas marked this conversation as resolved.
throw $failures[0];
}

return $handled;
}

/**
* Returns the in-memory transport registered under the given name.
*/
public static function getMessengerTransport(string $transport): InMemoryTransport
{
$container = static::getContainer();

if (!$container->has($id = 'messenger.transport.'.$transport)) {
static::fail(\sprintf('The "%s" Messenger transport is not registered. Did you forget to configure it under "framework.messenger.transports"?', $transport));
}

if (!($service = $container->get($id)) instanceof InMemoryTransport) {
static::fail(\sprintf('The "%s" Messenger transport is not an in-memory transport. Configure "in-memory://" as its DSN in the test environment to make queued message assertions.', $transport));
}

return $service;
}

/**
* Reads the messages sent for retry without waiting for their delay, so that a test does not have to.
*/
private static function createRetryAwareReceiver(InMemoryTransport $transport): ReceiverInterface
{
return new class($transport) implements ReceiverInterface {
public function __construct(
private InMemoryTransport $transport,
) {
}

/**
* @return list<Envelope>
*/
public function get(): iterable
{
$fetchSize = \func_num_args() > 0 ? max(1, func_get_arg(0)) : 1;
$envelopes = [];

foreach ($this->transport->get($fetchSize) as $envelope) {
$envelopes[] = $envelope;
}

if ($envelopes) {
return $envelopes;
}

foreach ($this->transport->all() as $envelope) {
if (!$envelope->last(RedeliveryStamp::class)) {
continue;
}

$envelopes[] = $envelope;

if (\count($envelopes) >= $fetchSize) {
break;
}
}

return $envelopes;
}

public function ack(Envelope $envelope): void
{
$this->transport->ack($envelope);
}

public function reject(Envelope $envelope): void
{
$this->transport->reject($envelope);
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger;

use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
class RecordingMessageHandler
{
public static array $handled = [];
public static int $attempts = 0;
public static int $failures = 0;

public function __invoke(FooMessage|BarMessage $message): void
{
++self::$attempts;

if (0 < self::$failures--) {
throw new \RuntimeException('Handling failed.');
}

self::$handled[] = $message;
}
}
146 changes: 146 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/Tests/Functional/MessengerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\FrameworkBundle\Tests\Functional;

use PHPUnit\Framework\AssertionFailedError;
use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\BarMessage;
use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\FooMessage;
use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\RecordingMessageHandler;
use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\SecondMessage;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Stamp\DelayStamp;
use Symfony\Component\Messenger\Transport\InMemory\InMemoryTransport;

class MessengerTest extends AbstractWebTestCase
{
protected function setUp(): void
{
RecordingMessageHandler::$handled = [];
RecordingMessageHandler::$attempts = 0;
RecordingMessageHandler::$failures = 0;
}

public function testQueuedMessagesAreConsumedThroughTheBus()
{
$bus = self::getContainer()->get(MessageBusInterface::class);
$bus->dispatch($foo = new FooMessage());
$bus->dispatch($bar = new BarMessage());

$this->assertInstanceOf(InMemoryTransport::class, $this->getMessengerTransport('async'));
$this->assertQueuedMessageCount(2, 'async');
$this->assertQueuedMessageCount(1, 'async', FooMessage::class);
$this->assertQueuedMessageCount(0, 'async', SecondMessage::class);
$this->assertSame([$foo, $bar], array_map(static fn (Envelope $envelope) => $envelope->getMessage(), $this->getQueuedMessages('async')));
$this->assertSame([], RecordingMessageHandler::$handled);

$this->assertSame(2, $this->consumeQueuedMessages('async'));

$this->assertQueuedMessageCount(0, 'async');
$this->assertSame([], $this->getQueuedMessages('async'));
$this->assertSame([$foo, $bar], RecordingMessageHandler::$handled);

$this->assertSame(0, $this->consumeQueuedMessages('async'));

$this->assertSame([$foo, $bar], RecordingMessageHandler::$handled, 'Consuming an empty queue returns without handling anything');
}

public function testConsumeQueuedMessagesWithLimit()
{
$bus = self::getContainer()->get(MessageBusInterface::class);
$bus->dispatch($foo1 = new FooMessage());
$bus->dispatch($foo2 = new FooMessage());
$bus->dispatch($foo3 = new FooMessage());

$this->consumeQueuedMessages('async', 2);

$this->assertQueuedMessageCount(1, 'async');
$this->assertSame([$foo1, $foo2], RecordingMessageHandler::$handled);

$this->consumeQueuedMessages('async', 2);

$this->assertQueuedMessageCount(0, 'async');
$this->assertSame([$foo1, $foo2, $foo3], RecordingMessageHandler::$handled);
}

public function testConsumeQueuedMessagesReplaysARetryUntilItSucceeds()
{
RecordingMessageHandler::$failures = 1;
self::getContainer()->get(MessageBusInterface::class)->dispatch($foo = new FooMessage());

$this->assertSame(1, $this->consumeQueuedMessages('async'));

// the retry was consumed without waiting for the 10 seconds of the retry strategy
$this->assertSame(2, RecordingMessageHandler::$attempts);
$this->assertSame([$foo], RecordingMessageHandler::$handled);
$this->assertQueuedMessageCount(0, 'async');
}

public function testConsumeQueuedMessagesRethrowsTheFailureThatExhaustsTheRetries()
{
RecordingMessageHandler::$failures = \PHP_INT_MAX;
self::getContainer()->get(MessageBusInterface::class)->dispatch(new FooMessage());

try {
$this->consumeQueuedMessages('async');
$this->fail('The failure that exhausts the retries should have been rethrown.');
} catch (HandlerFailedException $e) {
$this->assertSame('Handling failed.', array_values($e->getWrappedExceptions())[0]->getMessage());
}

// the default strategy allows three retries, and none of them waited
$this->assertSame(4, RecordingMessageHandler::$attempts);
$this->assertQueuedMessageCount(0, 'async');
$this->assertSame([], RecordingMessageHandler::$handled);
}

public function testConsumeQueuedMessagesLeavesAMessageDelayedByTheApplication()
{
self::getContainer()->get(MessageBusInterface::class)->dispatch(new FooMessage(), [new DelayStamp(3600000)]);

$this->assertSame(0, $this->consumeQueuedMessages('async'));

$this->assertQueuedMessageCount(1, 'async');
$this->assertSame(0, RecordingMessageHandler::$attempts);
}

public function testAssertQueuedMessageCountRejectsAnUnknownMessageClass()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('The message class "App\Message\Unknown" given to assertQueuedMessageCount() does not exist.');

$this->assertQueuedMessageCount(0, 'async', 'App\Message\Unknown');
}

public function testGetMessengerTransportRequiresAnInMemoryTransport()
{
$this->expectException(AssertionFailedError::class);
$this->expectExceptionMessage('The "sync" Messenger transport is not an in-memory transport. Configure "in-memory://" as its DSN in the test environment to make queued message assertions.');

$this->getMessengerTransport('sync');
}

public function testGetMessengerTransportRequiresAConfiguredTransport()
{
$this->expectException(AssertionFailedError::class);
$this->expectExceptionMessage('The "unknown" Messenger transport is not registered. Did you forget to configure it under "framework.messenger.transports"?');

$this->getMessengerTransport('unknown');
}

protected static function createKernel(array $options = []): KernelInterface
{
return parent::createKernel(['test_case' => 'Messenger'] + $options);
}
}
Loading