From 43aada9b42d797aabab937698a445b9d8dd3269b Mon Sep 17 00:00:00 2001 From: viktorprogger Date: Sat, 1 Nov 2025 20:03:02 +0500 Subject: [PATCH 01/17] Add Signal Loop test --- tests/Unit/Cli/SignalLoopTest.php | 138 ++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 tests/Unit/Cli/SignalLoopTest.php diff --git a/tests/Unit/Cli/SignalLoopTest.php b/tests/Unit/Cli/SignalLoopTest.php new file mode 100644 index 00000000..17254bd4 --- /dev/null +++ b/tests/Unit/Cli/SignalLoopTest.php @@ -0,0 +1,138 @@ +markTestSkipped('This rest requires PCNTL extension'); + } + + $loop = new SignalLoop(1); + self::assertFalse($loop->canContinue()); + } + + public function testSuspendAndResume(): void + { + if (!function_exists('pcntl_signal')) { + $this->markTestSkipped('pcntl not available'); + } + + pcntl_async_signals(true); + + $loop = new SignalLoop(0); + pcntl_signal(SIGALRM, static function (): void { + posix_kill(getmypid(), SIGCONT); + }); + + posix_kill(getmypid(), SIGTSTP); + pcntl_alarm(1); + + $start = microtime(true); + $result = $loop->canContinue(); + $elapsed = microtime(true) - $start; + + self::assertTrue($result); + self::assertGreaterThan(0.5, $elapsed); + } + + #[DataProvider('exitSignalProvider')] + public function testExitSignals(int $signal): void + { + if (!extension_loaded('pcntl')) { + $this->markTestSkipped('PCNTL extension not available'); + } + pcntl_async_signals(true); + + $loop = new SignalLoop(0); + + self::assertTrue($loop->canContinue(), 'Loop should continue'); + posix_kill(getmypid(), $signal); + + self::assertFalse($loop->canContinue(), "Loop should not continue after receiving signal {$signal}"); + } + + public static function exitSignalProvider(): iterable + { + yield 'SIGHUP' => [SIGHUP]; + yield 'SIGINT' => [SIGINT]; + yield 'SIGTERM' => [SIGTERM]; + } + + public function testResumeSignal(): void + { + if (!extension_loaded('pcntl')) { + $this->markTestSkipped('PCNTL extension not available'); + } + pcntl_async_signals(true); + + $loop = new SignalLoop(0); + + // First suspend the loop + posix_kill(getmypid(), SIGTSTP); + + // Then immediately resume + posix_kill(getmypid(), SIGCONT); + + $start = microtime(true); + $result = $loop->canContinue(); + $elapsed = microtime(true) - $start; + + self::assertTrue($result); + self::assertLessThan(0.1, $elapsed, 'Loop should resume quickly without waiting'); + } + + public function testMultipleExitSignals(): void + { + if (!extension_loaded('pcntl')) { + $this->markTestSkipped('PCNTL extension not available'); + } + pcntl_async_signals(true); + + $loop = new SignalLoop(0); + + // Send multiple exit signals + posix_kill(getmypid(), SIGINT); + posix_kill(getmypid(), SIGTERM); + + $result = $loop->canContinue(); + + self::assertFalse($result, 'Loop should not continue after receiving any exit signal'); + } + + public function testSuspendOverridesResume(): void + { + if (!extension_loaded('pcntl')) { + $this->markTestSkipped('PCNTL extension not available'); + } + pcntl_async_signals(true); + + $loop = new SignalLoop(0); + + // Resume first + posix_kill(getmypid(), SIGCONT); + // Then suspend + posix_kill(getmypid(), SIGTSTP); + + // Set up alarm to resume after 1 second + pcntl_signal(SIGALRM, static function (): void { + posix_kill(getmypid(), SIGCONT); + }); + pcntl_alarm(1); + + $start = microtime(true); + $result = $loop->canContinue(); + $elapsed = microtime(true) - $start; + + self::assertTrue($result); + self::assertGreaterThan(0.5, $elapsed, 'Loop should wait for resume after suspend'); + } +} From 539fa00b030eeec43b1f5b1ebcdad50bfe96b30b Mon Sep 17 00:00:00 2001 From: viktorprogger Date: Tue, 11 Nov 2025 18:58:35 +0500 Subject: [PATCH 02/17] Improve WorkerTest --- tests/Unit/WorkerTest.php | 188 +++++++++++++++++++++----------------- 1 file changed, 104 insertions(+), 84 deletions(-) diff --git a/tests/Unit/WorkerTest.php b/tests/Unit/WorkerTest.php index 6d84b10b..e6937470 100644 --- a/tests/Unit/WorkerTest.php +++ b/tests/Unit/WorkerTest.php @@ -4,8 +4,10 @@ namespace Yiisoft\Queue\Tests\Unit; +use PHPUnit\Framework\Attributes\DataProvider; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; use RuntimeException; use Yiisoft\Injector\Injector; use Yiisoft\Test\Support\Container\SimpleContainer; @@ -16,6 +18,9 @@ use Yiisoft\Queue\Middleware\Consume\ConsumeMiddlewareDispatcher; use Yiisoft\Queue\Middleware\Consume\MiddlewareFactoryConsumeInterface; use Yiisoft\Queue\Middleware\FailureHandling\FailureMiddlewareDispatcher; +use Yiisoft\Queue\Middleware\Consume\MiddlewareConsumeInterface; +use Yiisoft\Queue\Middleware\FailureHandling\FailureHandlingRequest; +use Yiisoft\Queue\Middleware\FailureHandling\MiddlewareFailureInterface; use Yiisoft\Queue\Middleware\FailureHandling\MiddlewareFactoryFailureInterface; use Yiisoft\Queue\QueueInterface; use Yiisoft\Queue\Tests\App\FakeHandler; @@ -25,87 +30,54 @@ final class WorkerTest extends TestCase { - public function testJobExecutedWithCallableHandler(): void + #[DataProvider('jobExecutedDataProvider')] + public function testJobExecuted($handler, array $containerServices): void { - $handleMessage = null; $message = new Message('simple', ['test-data']); $logger = new SimpleLogger(); - $container = new SimpleContainer(); - $handlers = [ - 'simple' => function (MessageInterface $message) use (&$handleMessage) { - $handleMessage = $message; - }, - ]; + $container = new SimpleContainer($containerServices); + $handlers = ['simple' => $handler]; + /** @var \PHPUnit\Framework\MockObject\MockObject&QueueInterface $queue */ $queue = $this->createMock(QueueInterface::class); - $worker = $this->createWorkerByParams($handlers, $logger, $container); + $worker = $this->createWorkerByParams($handlers, $container, $logger); $worker->process($message, $queue); - $this->assertSame($message, $handleMessage); + + $processedMessages = FakeHandler::$processedMessages; + FakeHandler::$processedMessages = []; + + $this->assertSame([$message], $processedMessages); $messages = $logger->getMessages(); $this->assertNotEmpty($messages); $this->assertStringContainsString('Processing message #null.', $messages[0]['message']); } - public function testJobExecutedWithDefinitionHandler(): void - { - $message = new Message('simple', ['test-data']); - $logger = new SimpleLogger(); - $handler = new FakeHandler(); - $container = new SimpleContainer([FakeHandler::class => $handler]); - $handlers = ['simple' => FakeHandler::class]; - - $queue = $this->createMock(QueueInterface::class); - $worker = $this->createWorkerByParams($handlers, $logger, $container); - - $worker->process($message, $queue); - $this->assertSame([$message], $handler::$processedMessages); - } - - public function testJobExecutedWithDefinitionClassHandler(): void - { - $message = new Message('simple', ['test-data']); - $logger = new SimpleLogger(); - $handler = new FakeHandler(); - $container = new SimpleContainer([FakeHandler::class => $handler]); - $handlers = ['simple' => [FakeHandler::class, 'execute']]; - - $queue = $this->createMock(QueueInterface::class); - $worker = $this->createWorkerByParams($handlers, $logger, $container); - - $worker->process($message, $queue); - $this->assertSame([$message], $handler::$processedMessages); - } - - public function testJobFailWithDefinitionNotFoundClassButExistInContainerHandler(): void - { - $message = new Message('simple', ['test-data']); - $logger = new SimpleLogger(); - $handler = new FakeHandler(); - $container = new SimpleContainer(['not-found-class-name' => $handler]); - $handlers = ['simple' => ['not-found-class-name', 'execute']]; - - $queue = $this->createMock(QueueInterface::class); - $worker = $this->createWorkerByParams($handlers, $logger, $container); - - $worker->process($message, $queue); - $this->assertSame([$message], $handler::$processedMessages); - } - - public function testJobExecutedWithStaticDefinitionHandler(): void + public static function jobExecutedDataProvider(): iterable { - $message = new Message('simple', ['test-data']); - $logger = new SimpleLogger(); - $handler = new FakeHandler(); - $container = new SimpleContainer([FakeHandler::class => $handler]); - $handlers = ['simple' => FakeHandler::staticExecute(...)]; - - $queue = $this->createMock(QueueInterface::class); - $worker = $this->createWorkerByParams($handlers, $logger, $container); - - $worker->process($message, $queue); - $this->assertSame([$message], $handler::$processedMessages); + yield 'definition' => [ + FakeHandler::class, + [FakeHandler::class => new FakeHandler()], + ]; + yield 'definition-class' => [ + [FakeHandler::class, 'execute'], + [FakeHandler::class => new FakeHandler()], + ]; + yield 'definition-not-found-class-but-exist-in-container' => [ + ['not-found-class-name', 'execute'], + ['not-found-class-name' => new FakeHandler()], + ]; + yield 'static-definition' => [ + [FakeHandler::class, 'staticExecute'], + [FakeHandler::class => new FakeHandler()], + ]; + yield 'callable' => [ + function (MessageInterface $message) { + FakeHandler::$processedMessages[] = $message; + }, + [], + ]; } public function testJobFailWithDefinitionUndefinedMethodHandler(): void @@ -113,13 +85,13 @@ public function testJobFailWithDefinitionUndefinedMethodHandler(): void $this->expectExceptionMessage('Queue handler with name "simple" does not exist'); $message = new Message('simple', ['test-data']); - $logger = new SimpleLogger(); $handler = new FakeHandler(); $container = new SimpleContainer([FakeHandler::class => $handler]); $handlers = ['simple' => [FakeHandler::class, 'undefinedMethod']]; + /** @var \PHPUnit\Framework\MockObject\MockObject&QueueInterface $queue */ $queue = $this->createMock(QueueInterface::class); - $worker = $this->createWorkerByParams($handlers, $logger, $container); + $worker = $this->createWorkerByParams($handlers, $container); $worker->process($message, $queue); } @@ -134,8 +106,9 @@ public function testJobFailWithDefinitionUndefinedClassHandler(): void $container = new SimpleContainer([FakeHandler::class => $handler]); $handlers = ['simple' => ['UndefinedClass', 'handle']]; + /** @var \PHPUnit\Framework\MockObject\MockObject&QueueInterface $queue */ $queue = $this->createMock(QueueInterface::class); - $worker = $this->createWorkerByParams($handlers, $logger, $container); + $worker = $this->createWorkerByParams($handlers, $container, $logger); try { $worker->process($message, $queue); @@ -150,12 +123,12 @@ public function testJobFailWithDefinitionClassNotFoundInContainerHandler(): void { $this->expectExceptionMessage('Queue handler with name "simple" does not exist'); $message = new Message('simple', ['test-data']); - $logger = new SimpleLogger(); $container = new SimpleContainer(); $handlers = ['simple' => [FakeHandler::class, 'execute']]; + /** @var \PHPUnit\Framework\MockObject\MockObject&QueueInterface $queue */ $queue = $this->createMock(QueueInterface::class); - $worker = $this->createWorkerByParams($handlers, $logger, $container); + $worker = $this->createWorkerByParams($handlers, $container); $worker->process($message, $queue); } @@ -168,8 +141,9 @@ public function testJobFailWithDefinitionHandlerException(): void $container = new SimpleContainer([FakeHandler::class => $handler]); $handlers = ['simple' => [FakeHandler::class, 'executeWithException']]; + /** @var \PHPUnit\Framework\MockObject\MockObject&QueueInterface $queue */ $queue = $this->createMock(QueueInterface::class); - $worker = $this->createWorkerByParams($handlers, $logger, $container); + $worker = $this->createWorkerByParams($handlers, $container, $logger); try { $worker->process($message, $queue); @@ -189,28 +163,33 @@ public function testJobFailWithDefinitionHandlerException(): void private function createWorkerByParams( array $handlers, - LoggerInterface $logger, - ContainerInterface $container + ContainerInterface $container, + ?LoggerInterface $logger = null, ): Worker { + /** @var \PHPUnit\Framework\MockObject\MockObject&MiddlewareFactoryConsumeInterface $consumeMiddlewareFactory */ + $consumeMiddlewareFactory = $this->createMock(MiddlewareFactoryConsumeInterface::class); + /** @var \PHPUnit\Framework\MockObject\MockObject&MiddlewareFactoryFailureInterface $failureMiddlewareFactory */ + $failureMiddlewareFactory = $this->createMock(MiddlewareFactoryFailureInterface::class); + return new Worker( $handlers, - $logger, + $logger ?? new NullLogger(), new Injector($container), $container, - new ConsumeMiddlewareDispatcher($this->createMock(MiddlewareFactoryConsumeInterface::class)), - new FailureMiddlewareDispatcher($this->createMock(MiddlewareFactoryFailureInterface::class), []), + new ConsumeMiddlewareDispatcher($consumeMiddlewareFactory), + new FailureMiddlewareDispatcher($failureMiddlewareFactory, []), ); } public function testHandlerNotFoundInContainer(): void { $message = new Message('nonexistent', ['test-data']); - $logger = new SimpleLogger(); $container = new SimpleContainer(); $handlers = []; + /** @var \PHPUnit\Framework\MockObject\MockObject&QueueInterface $queue */ $queue = $this->createMock(QueueInterface::class); - $worker = $this->createWorkerByParams($handlers, $logger, $container); + $worker = $this->createWorkerByParams($handlers, $container); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Queue handler with name "nonexistent" does not exist'); @@ -220,7 +199,6 @@ public function testHandlerNotFoundInContainer(): void public function testHandlerInContainerNotImplementingInterface(): void { $message = new Message('invalid', ['test-data']); - $logger = new SimpleLogger(); $container = new SimpleContainer([ 'invalid' => new class () { public function handle(): void @@ -230,25 +208,67 @@ public function handle(): void ]); $handlers = []; + /** @var \PHPUnit\Framework\MockObject\MockObject&QueueInterface $queue */ $queue = $this->createMock(QueueInterface::class); - $worker = $this->createWorkerByParams($handlers, $logger, $container); + $worker = $this->createWorkerByParams($handlers, $container); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Queue handler with name "invalid" does not exist'); $worker->process($message, $queue); } + public function testJobFailureIsHandledSuccessfully(): void + { + $message = new Message('simple', null); + /** @var \PHPUnit\Framework\MockObject\MockObject&QueueInterface $queue */ + $queue = $this->createMock(QueueInterface::class); + $queue->method('getChannel')->willReturn('test-channel'); + + $originalException = new RuntimeException('Consume failed'); + /** @var \PHPUnit\Framework\MockObject\MockObject&MiddlewareConsumeInterface $consumeMiddleware */ + $consumeMiddleware = $this->createMock(MiddlewareConsumeInterface::class); + $consumeMiddleware->method('processConsume')->willThrowException($originalException); + + /** @var \PHPUnit\Framework\MockObject\MockObject&MiddlewareFactoryConsumeInterface $consumeMiddlewareFactory */ + $consumeMiddlewareFactory = $this->createMock(MiddlewareFactoryConsumeInterface::class); + $consumeMiddlewareFactory->method('createConsumeMiddleware')->willReturn($consumeMiddleware); + $consumeDispatcher = new ConsumeMiddlewareDispatcher($consumeMiddlewareFactory, 'simple'); + + $finalMessage = new Message('final', null); + /** @var \PHPUnit\Framework\MockObject\MockObject&MiddlewareFailureInterface $failureMiddleware */ + $failureMiddleware = $this->createMock(MiddlewareFailureInterface::class); + $failureMiddleware->method('processFailure')->willReturn(new FailureHandlingRequest($finalMessage, $originalException, $queue)); + + /** @var \PHPUnit\Framework\MockObject\MockObject&MiddlewareFactoryFailureInterface $failureMiddlewareFactory */ + $failureMiddlewareFactory = $this->createMock(MiddlewareFactoryFailureInterface::class); + $failureMiddlewareFactory->method('createFailureMiddleware')->willReturn($failureMiddleware); + $failureDispatcher = new FailureMiddlewareDispatcher($failureMiddlewareFactory, ['test-channel' => ['simple']]); + + $worker = new Worker( + ['simple' => fn () => null], + new NullLogger(), + new Injector(new SimpleContainer()), + new SimpleContainer(), + $consumeDispatcher, + $failureDispatcher + ); + + $result = $worker->process($message, $queue); + + self::assertSame($finalMessage, $result); + } + public function testStaticMethodHandler(): void { $message = new Message('static-handler', ['test-data']); - $logger = new SimpleLogger(); $container = new SimpleContainer(); $handlers = [ 'static-handler' => StaticMessageHandler::handle(...), ]; + /** @var \PHPUnit\Framework\MockObject\MockObject&QueueInterface $queue */ $queue = $this->createMock(QueueInterface::class); - $worker = $this->createWorkerByParams($handlers, $logger, $container); + $worker = $this->createWorkerByParams($handlers, $container); StaticMessageHandler::$wasHandled = false; $worker->process($message, $queue); From 56e8450bf86b0df1555e7936460f9d385d45e11a Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Tue, 11 Nov 2025 13:58:48 +0000 Subject: [PATCH 03/17] Apply fixes from StyleCI --- tests/Unit/WorkerTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Unit/WorkerTest.php b/tests/Unit/WorkerTest.php index e6937470..14ed07a5 100644 --- a/tests/Unit/WorkerTest.php +++ b/tests/Unit/WorkerTest.php @@ -166,9 +166,9 @@ private function createWorkerByParams( ContainerInterface $container, ?LoggerInterface $logger = null, ): Worker { - /** @var \PHPUnit\Framework\MockObject\MockObject&MiddlewareFactoryConsumeInterface $consumeMiddlewareFactory */ + /** @var MiddlewareFactoryConsumeInterface&\PHPUnit\Framework\MockObject\MockObject $consumeMiddlewareFactory */ $consumeMiddlewareFactory = $this->createMock(MiddlewareFactoryConsumeInterface::class); - /** @var \PHPUnit\Framework\MockObject\MockObject&MiddlewareFactoryFailureInterface $failureMiddlewareFactory */ + /** @var MiddlewareFactoryFailureInterface&\PHPUnit\Framework\MockObject\MockObject $failureMiddlewareFactory */ $failureMiddlewareFactory = $this->createMock(MiddlewareFactoryFailureInterface::class); return new Worker( @@ -225,21 +225,21 @@ public function testJobFailureIsHandledSuccessfully(): void $queue->method('getChannel')->willReturn('test-channel'); $originalException = new RuntimeException('Consume failed'); - /** @var \PHPUnit\Framework\MockObject\MockObject&MiddlewareConsumeInterface $consumeMiddleware */ + /** @var MiddlewareConsumeInterface&\PHPUnit\Framework\MockObject\MockObject $consumeMiddleware */ $consumeMiddleware = $this->createMock(MiddlewareConsumeInterface::class); $consumeMiddleware->method('processConsume')->willThrowException($originalException); - /** @var \PHPUnit\Framework\MockObject\MockObject&MiddlewareFactoryConsumeInterface $consumeMiddlewareFactory */ + /** @var MiddlewareFactoryConsumeInterface&\PHPUnit\Framework\MockObject\MockObject $consumeMiddlewareFactory */ $consumeMiddlewareFactory = $this->createMock(MiddlewareFactoryConsumeInterface::class); $consumeMiddlewareFactory->method('createConsumeMiddleware')->willReturn($consumeMiddleware); $consumeDispatcher = new ConsumeMiddlewareDispatcher($consumeMiddlewareFactory, 'simple'); $finalMessage = new Message('final', null); - /** @var \PHPUnit\Framework\MockObject\MockObject&MiddlewareFailureInterface $failureMiddleware */ + /** @var MiddlewareFailureInterface&\PHPUnit\Framework\MockObject\MockObject $failureMiddleware */ $failureMiddleware = $this->createMock(MiddlewareFailureInterface::class); $failureMiddleware->method('processFailure')->willReturn(new FailureHandlingRequest($finalMessage, $originalException, $queue)); - /** @var \PHPUnit\Framework\MockObject\MockObject&MiddlewareFactoryFailureInterface $failureMiddlewareFactory */ + /** @var MiddlewareFactoryFailureInterface&\PHPUnit\Framework\MockObject\MockObject $failureMiddlewareFactory */ $failureMiddlewareFactory = $this->createMock(MiddlewareFactoryFailureInterface::class); $failureMiddlewareFactory->method('createFailureMiddleware')->willReturn($failureMiddleware); $failureDispatcher = new FailureMiddlewareDispatcher($failureMiddlewareFactory, ['test-channel' => ['simple']]); From 9540389a9b1d620a24cb7009cf428fc0e3cd9063 Mon Sep 17 00:00:00 2001 From: viktorprogger Date: Tue, 11 Nov 2025 19:00:01 +0500 Subject: [PATCH 04/17] Improve QueueProviderInterfaceProxyTest --- tests/Unit/Debug/QueueProviderInterfaceProxyTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/Unit/Debug/QueueProviderInterfaceProxyTest.php b/tests/Unit/Debug/QueueProviderInterfaceProxyTest.php index 2bde59c5..8c404857 100644 --- a/tests/Unit/Debug/QueueProviderInterfaceProxyTest.php +++ b/tests/Unit/Debug/QueueProviderInterfaceProxyTest.php @@ -23,4 +23,14 @@ public function testGet(): void $this->assertInstanceOf(QueueDecorator::class, $factory->get('test')); } + + public function testHas(): void + { + $queueFactory = $this->createMock(QueueProviderInterface::class); + $queueFactory->expects($this->once())->method('has')->with('test')->willReturn(true); + $collector = new QueueCollector(); + $factory = new QueueProviderInterfaceProxy($queueFactory, $collector); + + $this->assertTrue($factory->has('test')); + } } From f5c5d19de9123ccbdb49cfecf143374d48a27a3e Mon Sep 17 00:00:00 2001 From: viktorprogger Date: Tue, 11 Nov 2025 19:17:32 +0500 Subject: [PATCH 05/17] Improve SignalLoopTest --- tests/Unit/Cli/SignalLoopTest.php | 66 +++++++++++-------------------- 1 file changed, 22 insertions(+), 44 deletions(-) diff --git a/tests/Unit/Cli/SignalLoopTest.php b/tests/Unit/Cli/SignalLoopTest.php index 17254bd4..37ed71ba 100644 --- a/tests/Unit/Cli/SignalLoopTest.php +++ b/tests/Unit/Cli/SignalLoopTest.php @@ -6,34 +6,32 @@ use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; use Yiisoft\Queue\Cli\SignalLoop; +#[RequiresPhpExtension('pcntl')] final class SignalLoopTest extends TestCase { - public function testMemoryLimitReached(): void + public function setUp(): void { - if (!extension_loaded('pcntl')) { - $this->markTestSkipped('This rest requires PCNTL extension'); - } + parent::setUp(); + pcntl_async_signals(true); + } + public function testMemoryLimitReached(): void + { $loop = new SignalLoop(1); self::assertFalse($loop->canContinue()); } public function testSuspendAndResume(): void { - if (!function_exists('pcntl_signal')) { - $this->markTestSkipped('pcntl not available'); - } - - pcntl_async_signals(true); - $loop = new SignalLoop(0); - pcntl_signal(SIGALRM, static function (): void { - posix_kill(getmypid(), SIGCONT); + pcntl_signal(\SIGALRM, static function (): void { + posix_kill(getmypid(), \SIGCONT); }); - posix_kill(getmypid(), SIGTSTP); + posix_kill(getmypid(), \SIGTSTP); pcntl_alarm(1); $start = microtime(true); @@ -47,11 +45,6 @@ public function testSuspendAndResume(): void #[DataProvider('exitSignalProvider')] public function testExitSignals(int $signal): void { - if (!extension_loaded('pcntl')) { - $this->markTestSkipped('PCNTL extension not available'); - } - pcntl_async_signals(true); - $loop = new SignalLoop(0); self::assertTrue($loop->canContinue(), 'Loop should continue'); @@ -62,25 +55,20 @@ public function testExitSignals(int $signal): void public static function exitSignalProvider(): iterable { - yield 'SIGHUP' => [SIGHUP]; - yield 'SIGINT' => [SIGINT]; - yield 'SIGTERM' => [SIGTERM]; + yield 'SIGHUP' => [\SIGHUP]; + yield 'SIGINT' => [\SIGINT]; + yield 'SIGTERM' => [\SIGTERM]; } public function testResumeSignal(): void { - if (!extension_loaded('pcntl')) { - $this->markTestSkipped('PCNTL extension not available'); - } - pcntl_async_signals(true); - $loop = new SignalLoop(0); // First suspend the loop - posix_kill(getmypid(), SIGTSTP); + posix_kill(getmypid(), \SIGTSTP); // Then immediately resume - posix_kill(getmypid(), SIGCONT); + posix_kill(getmypid(), \SIGCONT); $start = microtime(true); $result = $loop->canContinue(); @@ -92,16 +80,11 @@ public function testResumeSignal(): void public function testMultipleExitSignals(): void { - if (!extension_loaded('pcntl')) { - $this->markTestSkipped('PCNTL extension not available'); - } - pcntl_async_signals(true); - $loop = new SignalLoop(0); // Send multiple exit signals - posix_kill(getmypid(), SIGINT); - posix_kill(getmypid(), SIGTERM); + posix_kill(getmypid(), \SIGINT); + posix_kill(getmypid(), \SIGTERM); $result = $loop->canContinue(); @@ -110,21 +93,16 @@ public function testMultipleExitSignals(): void public function testSuspendOverridesResume(): void { - if (!extension_loaded('pcntl')) { - $this->markTestSkipped('PCNTL extension not available'); - } - pcntl_async_signals(true); - $loop = new SignalLoop(0); // Resume first - posix_kill(getmypid(), SIGCONT); + posix_kill(getmypid(), \SIGCONT); // Then suspend - posix_kill(getmypid(), SIGTSTP); + posix_kill(getmypid(), \SIGTSTP); // Set up alarm to resume after 1 second - pcntl_signal(SIGALRM, static function (): void { - posix_kill(getmypid(), SIGCONT); + pcntl_signal(\SIGALRM, static function (): void { + posix_kill(getmypid(), \SIGCONT); }); pcntl_alarm(1); From acfbff35feb157fd89b1ac8c0cab222374816cd0 Mon Sep 17 00:00:00 2001 From: viktorprogger Date: Tue, 11 Nov 2025 19:17:53 +0500 Subject: [PATCH 06/17] Add QueueWorkerInterfaceProxyTest --- .../Debug/QueueWorkerInterfaceProxyTest.php | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/Unit/Debug/QueueWorkerInterfaceProxyTest.php diff --git a/tests/Unit/Debug/QueueWorkerInterfaceProxyTest.php b/tests/Unit/Debug/QueueWorkerInterfaceProxyTest.php new file mode 100644 index 00000000..acd63e3e --- /dev/null +++ b/tests/Unit/Debug/QueueWorkerInterfaceProxyTest.php @@ -0,0 +1,33 @@ +startup(); + $proxy = new QueueWorkerInterfaceProxy(new StubWorker(), $collector); + + $result = $proxy->process($message, new DummyQueue('chan')); + + self::assertSame($message, $result); + + $collected = $collector->getCollected(); + self::assertArrayHasKey('processingMessages', $collected); + self::assertArrayHasKey('chan', $collected['processingMessages']); + self::assertCount(1, $collected['processingMessages']['chan']); + self::assertSame($message, $collected['processingMessages']['chan'][0]); + } +} From 07dd1b3bc32e26b6c58afbea21594ac1acd47286 Mon Sep 17 00:00:00 2001 From: viktorprogger Date: Tue, 11 Nov 2025 19:25:09 +0500 Subject: [PATCH 07/17] Improve envelope tests --- .../{EnvelopeTraitTest.php => EnvelopeTest.php} | 15 ++++++++++++++- tests/Unit/Message/JsonMessageSerializerTest.php | 10 ++++------ 2 files changed, 18 insertions(+), 7 deletions(-) rename tests/Unit/Message/{EnvelopeTraitTest.php => EnvelopeTest.php} (55%) diff --git a/tests/Unit/Message/EnvelopeTraitTest.php b/tests/Unit/Message/EnvelopeTest.php similarity index 55% rename from tests/Unit/Message/EnvelopeTraitTest.php rename to tests/Unit/Message/EnvelopeTest.php index 6ae7f085..0c54f4cc 100644 --- a/tests/Unit/Message/EnvelopeTraitTest.php +++ b/tests/Unit/Message/EnvelopeTest.php @@ -6,8 +6,11 @@ use PHPUnit\Framework\TestCase; use Yiisoft\Queue\Tests\App\DummyEnvelope; +use Yiisoft\Queue\Message\EnvelopeInterface; +use Yiisoft\Queue\Message\IdEnvelope; +use Yiisoft\Queue\Message\Message; -final class EnvelopeTraitTest extends TestCase +final class EnvelopeTest extends TestCase { public function testFromData(): void { @@ -23,4 +26,14 @@ public function testFromData(): void $this->assertArrayHasKey('meta', $envelope->getMetadata()); $this->assertSame('data', $envelope->getMetadata()['meta']); } + + public function testNonArrayStackIsNormalized(): void + { + $base = new Message('handler', 'data', [EnvelopeInterface::ENVELOPE_STACK_KEY => 'oops']); + $wrapped = new DummyEnvelope($base, 'id-1'); + + $meta = $wrapped->getMetadata(); + self::assertIsArray($meta[EnvelopeInterface::ENVELOPE_STACK_KEY]); + self::assertSame([DummyEnvelope::class], $meta[EnvelopeInterface::ENVELOPE_STACK_KEY]); + } } diff --git a/tests/Unit/Message/JsonMessageSerializerTest.php b/tests/Unit/Message/JsonMessageSerializerTest.php index 46617994..e2495245 100644 --- a/tests/Unit/Message/JsonMessageSerializerTest.php +++ b/tests/Unit/Message/JsonMessageSerializerTest.php @@ -67,9 +67,7 @@ public function testDefaultMessageClassFallbackClassNotSet(): void $this->assertInstanceOf(Message::class, $message); } - /** - * @dataProvider dataUnsupportedPayloadFormat - */ + #[DataProvider('dataUnsupportedPayloadFormat')] public function testPayloadFormat(mixed $payload): void { $serializer = $this->createSerializer(); @@ -87,9 +85,7 @@ public static function dataUnsupportedPayloadFormat(): iterable yield 'null' => [null]; } - /** - * @dataProvider dataUnsupportedMetadataFormat - */ + #[DataProvider('dataUnsupportedMetadataFormat')] public function testMetadataFormat(mixed $meta): void { $payload = ['name' => 'handler', 'data' => 'test', 'meta' => $meta]; @@ -142,6 +138,7 @@ public function testUnserializeEnvelopeStack(): void ]; $serializer = $this->createSerializer(); + /** @var IdEnvelope $message */ $message = $serializer->unserialize(json_encode($payload)); $this->assertEquals($payload['data'], $message->getData()); @@ -185,6 +182,7 @@ public function testSerializeEnvelopeStack(): void $json, ); + /** @var IdEnvelope $message */ $message = $serializer->unserialize($json); $this->assertInstanceOf(IdEnvelope::class, $message); From 020e256417e7f6b089f76b58fac420fbd6a6de98 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Tue, 11 Nov 2025 14:25:22 +0000 Subject: [PATCH 08/17] Apply fixes from StyleCI --- tests/Unit/Message/EnvelopeTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Unit/Message/EnvelopeTest.php b/tests/Unit/Message/EnvelopeTest.php index 0c54f4cc..9dbba5f8 100644 --- a/tests/Unit/Message/EnvelopeTest.php +++ b/tests/Unit/Message/EnvelopeTest.php @@ -7,7 +7,6 @@ use PHPUnit\Framework\TestCase; use Yiisoft\Queue\Tests\App\DummyEnvelope; use Yiisoft\Queue\Message\EnvelopeInterface; -use Yiisoft\Queue\Message\IdEnvelope; use Yiisoft\Queue\Message\Message; final class EnvelopeTest extends TestCase From 8cbf0a112769dcccc18ddc5eae5288204cda6a65 Mon Sep 17 00:00:00 2001 From: viktorprogger <7670669+viktorprogger@users.noreply.github.com> Date: Tue, 11 Nov 2025 14:26:17 +0000 Subject: [PATCH 09/17] Apply Rector changes (CI) --- tests/Unit/WorkerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/WorkerTest.php b/tests/Unit/WorkerTest.php index 14ed07a5..b8f466f6 100644 --- a/tests/Unit/WorkerTest.php +++ b/tests/Unit/WorkerTest.php @@ -69,7 +69,7 @@ public static function jobExecutedDataProvider(): iterable ['not-found-class-name' => new FakeHandler()], ]; yield 'static-definition' => [ - [FakeHandler::class, 'staticExecute'], + FakeHandler::staticExecute(...), [FakeHandler::class => new FakeHandler()], ]; yield 'callable' => [ From 9eb07c8e4b155106fb800eee94a66d5d782ca969 Mon Sep 17 00:00:00 2001 From: viktorprogger Date: Tue, 11 Nov 2025 19:32:13 +0500 Subject: [PATCH 10/17] Improve FailureHandlingRequestTest --- .../FailureHandlingRequestTest.php | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/Unit/Middleware/FailureHandling/FailureHandlingRequestTest.php b/tests/Unit/Middleware/FailureHandling/FailureHandlingRequestTest.php index 5ee524ac..534111ee 100644 --- a/tests/Unit/Middleware/FailureHandling/FailureHandlingRequestTest.php +++ b/tests/Unit/Middleware/FailureHandling/FailureHandlingRequestTest.php @@ -15,12 +15,23 @@ final class FailureHandlingRequestTest extends TestCase public function testImmutable(): void { $queue = $this->createMock(QueueInterface::class); - $failureHandlingRequest = new FailureHandlingRequest( - new Message('test', 'test'), - new Exception(), + $request1 = new FailureHandlingRequest( + new Message('test', null), + new Exception('exception 1'), $queue ); + $request2 = $request1->withQueue($queue); + $request3 = $request1->withException(new Exception('exception 2')); + $request4 = $request1->withMessage(new Message('test2', null)); - $this->assertNotSame($failureHandlingRequest, $failureHandlingRequest->withQueue($queue)); + $this->assertNotSame($request1, $request2); + + $this->assertNotSame($request1, $request3); + $this->assertEquals($request1->getException()->getMessage(), 'exception 1'); + $this->assertEquals($request3->getException()->getMessage(), 'exception 2'); + + $this->assertNotSame($request1, $request4); + $this->assertEquals($request1->getMessage()->getHandlerName(), 'test'); + $this->assertEquals($request4->getMessage()->getHandlerName(), 'test2'); } } From bca11d3ca1da334c565e557f8128b4645ffc3602 Mon Sep 17 00:00:00 2001 From: viktorprogger Date: Tue, 11 Nov 2025 19:42:50 +0500 Subject: [PATCH 11/17] Add CallableFactoryTest --- tests/Unit/Middleware/CallableFactoryTest.php | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/Unit/Middleware/CallableFactoryTest.php diff --git a/tests/Unit/Middleware/CallableFactoryTest.php b/tests/Unit/Middleware/CallableFactoryTest.php new file mode 100644 index 00000000..e77e6b81 --- /dev/null +++ b/tests/Unit/Middleware/CallableFactoryTest.php @@ -0,0 +1,74 @@ + $invokable, + ]); + + $factory = new CallableFactory($container); + $callable = $factory->create('invokable'); + + self::assertIsCallable($callable); + self::assertSame('ok', $callable()); + } + + public function testCreateFromStaticMethodArray(): void + { + $class = new class () { + public static function ping(): string + { + return 'pong'; + } + }; + $className = $class::class; + $container = new SimpleContainer(); + + $factory = new CallableFactory($container); + $callable = $factory->create([$className, 'ping']); + + self::assertIsCallable($callable); + self::assertSame('pong', $callable()); + } + public function testCreateFromContainerObjectMethod(): void + { + $service = new class () { + public function go(): string { return 'ok'; } + }; + $className = $service::class; + $container = new SimpleContainer([ + $className => $service, + ]); + + $factory = new CallableFactory($container); + $callable = $factory->create([$className, 'go']); + + self::assertIsCallable($callable); + self::assertSame('ok', $callable()); + } + + public function testFriendlyException(): void + { + $e = new InvalidCallableConfigurationException(); + self::assertSame('Invalid callable configuration.', $e->getName()); + self::assertNotNull($e->getSolution()); + self::assertStringContainsString('callable', (string)$e->getSolution()); + } +} From f97ccd7977f5bd0cc68ac1236dc3710ed4759841 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Tue, 11 Nov 2025 14:42:58 +0000 Subject: [PATCH 12/17] Apply fixes from StyleCI --- tests/Unit/Middleware/CallableFactoryTest.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/Unit/Middleware/CallableFactoryTest.php b/tests/Unit/Middleware/CallableFactoryTest.php index e77e6b81..ce8247ad 100644 --- a/tests/Unit/Middleware/CallableFactoryTest.php +++ b/tests/Unit/Middleware/CallableFactoryTest.php @@ -47,10 +47,14 @@ public static function ping(): string self::assertIsCallable($callable); self::assertSame('pong', $callable()); } + public function testCreateFromContainerObjectMethod(): void { $service = new class () { - public function go(): string { return 'ok'; } + public function go(): string + { + return 'ok'; + } }; $className = $service::class; $container = new SimpleContainer([ From 15fd74cc04fefaae0dd6b9c2897eda12077d8b40 Mon Sep 17 00:00:00 2001 From: viktorprogger Date: Tue, 11 Nov 2025 19:44:33 +0500 Subject: [PATCH 13/17] Add AdapterPushHandlerTest --- .../Push/AdapterPushHandlerTest.php | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 tests/Unit/Middleware/Push/AdapterPushHandlerTest.php diff --git a/tests/Unit/Middleware/Push/AdapterPushHandlerTest.php b/tests/Unit/Middleware/Push/AdapterPushHandlerTest.php new file mode 100644 index 00000000..3d41ecbb --- /dev/null +++ b/tests/Unit/Middleware/Push/AdapterPushHandlerTest.php @@ -0,0 +1,37 @@ +expectException(AdapterNotConfiguredException::class); + $handler->handlePush($request); + } + + public function testHandlePushUsesAdapter(): void + { + $handler = new AdapterPushHandler(); + $adapter = new FakeAdapter(); + $message = new Message('handler', 'data'); + $request = new PushRequest($message, $adapter); + + $result = $handler->handlePush($request); + + self::assertSame($message, $result->getMessage()); + self::assertSame([$message], $adapter->pushMessages); + } +} From 6ba6a5c13856b0614d6df67f176a0a226f4f71db Mon Sep 17 00:00:00 2001 From: viktorprogger Date: Tue, 11 Nov 2025 19:49:51 +0500 Subject: [PATCH 14/17] Fix Queue::withAdapter() return type --- src/Debug/QueueDecorator.php | 2 +- src/Queue.php | 2 +- src/QueueInterface.php | 2 +- stubs/StubQueue.php | 2 +- tests/App/DummyQueue.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Debug/QueueDecorator.php b/src/Debug/QueueDecorator.php index adc8e6b1..0c4294e6 100644 --- a/src/Debug/QueueDecorator.php +++ b/src/Debug/QueueDecorator.php @@ -45,7 +45,7 @@ public function listen(): void $this->queue->listen(); } - public function withAdapter(AdapterInterface $adapter): QueueInterface + public function withAdapter(AdapterInterface $adapter): static { return new self($this->queue->withAdapter($adapter), $this->collector); } diff --git a/src/Queue.php b/src/Queue.php index faf890df..5088c3eb 100644 --- a/src/Queue.php +++ b/src/Queue.php @@ -109,7 +109,7 @@ public function status(string|int $id): JobStatus return $this->adapter->status($id); } - public function withAdapter(AdapterInterface $adapter): self + public function withAdapter(AdapterInterface $adapter): static { $new = clone $this; $new->adapter = $adapter; diff --git a/src/QueueInterface.php b/src/QueueInterface.php index 3c8b9a3a..f31d36c0 100644 --- a/src/QueueInterface.php +++ b/src/QueueInterface.php @@ -43,7 +43,7 @@ public function listen(): void; */ public function status(string|int $id): JobStatus; - public function withAdapter(AdapterInterface $adapter): self; + public function withAdapter(AdapterInterface $adapter): static; public function getChannel(): string; } diff --git a/stubs/StubQueue.php b/stubs/StubQueue.php index dc19ed62..4d2ed10a 100644 --- a/stubs/StubQueue.php +++ b/stubs/StubQueue.php @@ -46,7 +46,7 @@ public function getAdapter(): ?AdapterInterface return $this->adapter; } - public function withAdapter(AdapterInterface $adapter): QueueInterface + public function withAdapter(AdapterInterface $adapter): static { $new = clone $this; $new->adapter = $adapter; diff --git a/tests/App/DummyQueue.php b/tests/App/DummyQueue.php index 149845e7..579afb13 100644 --- a/tests/App/DummyQueue.php +++ b/tests/App/DummyQueue.php @@ -39,7 +39,7 @@ public function status(string|int $id): JobStatus throw new Exception('`status()` method is not implemented yet.'); } - public function withAdapter(AdapterInterface $adapter): QueueInterface + public function withAdapter(AdapterInterface $adapter): static { throw new Exception('`withAdapter()` method is not implemented yet.'); } From 30ea1453b1bfed9b7f4bc68d8593937542ac884a Mon Sep 17 00:00:00 2001 From: viktorprogger Date: Tue, 11 Nov 2025 19:50:09 +0500 Subject: [PATCH 15/17] Update phpunit to version 12 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index fd3e594f..100d37c6 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,7 @@ "require-dev": { "maglnet/composer-require-checker": "^4.7.1", "phpbench/phpbench": "^1.4.1", - "phpunit/phpunit": "^10.5.45", + "phpunit/phpunit": "^12.4", "rector/rector": "^2.0.11", "roave/infection-static-analysis-plugin": "^1.35", "spatie/phpunit-watcher": "^1.24", From e1e3bd653ee6f7b97914b298a55d8a6a60f56555 Mon Sep 17 00:00:00 2001 From: viktorprogger Date: Tue, 11 Nov 2025 19:51:45 +0500 Subject: [PATCH 16/17] Fix phpunit version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 100d37c6..d3e1ca15 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,7 @@ "require-dev": { "maglnet/composer-require-checker": "^4.7.1", "phpbench/phpbench": "^1.4.1", - "phpunit/phpunit": "^12.4", + "phpunit/phpunit": "^10.5.45 || ^12.4", "rector/rector": "^2.0.11", "roave/infection-static-analysis-plugin": "^1.35", "spatie/phpunit-watcher": "^1.24", From e672fb44b5f132f060543182bf202ee331df2e9a Mon Sep 17 00:00:00 2001 From: viktorprogger Date: Sat, 13 Dec 2025 09:09:10 +0500 Subject: [PATCH 17/17] Don't use phpunit 12 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d3e1ca15..fd3e594f 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,7 @@ "require-dev": { "maglnet/composer-require-checker": "^4.7.1", "phpbench/phpbench": "^1.4.1", - "phpunit/phpunit": "^10.5.45 || ^12.4", + "phpunit/phpunit": "^10.5.45", "rector/rector": "^2.0.11", "roave/infection-static-analysis-plugin": "^1.35", "spatie/phpunit-watcher": "^1.24",