From 3d67cc77f2d56984f38fa8fe98d89a48a5aee4bf Mon Sep 17 00:00:00 2001 From: Kevin Pfeifer Date: Sun, 1 Sep 2024 10:07:48 +0200 Subject: [PATCH 1/6] add PHPUnit 11 support --- .github/workflows/ci.yml | 1 + composer.json | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f92d696..89a5195 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: - 'main' - '1.x' + - '1.next' pull_request: branches: - '*' diff --git a/composer.json b/composer.json index 206e117..3b1f754 100644 --- a/composer.json +++ b/composer.json @@ -4,12 +4,12 @@ "type": "cakephp-plugin", "require": { "php": "^8.1", - "cakephp/cakephp": "^5.0.0", + "cakephp/cakephp": "dev-5.next", "dragonmantank/cron-expression": "^3.3" }, "require-dev": { "cakephp/cakephp-codesniffer": "^5.0", - "phpunit/phpunit": "^10.1" + "phpunit/phpunit": "^10.5.5 || ^11.1.3" }, "license": "MIT", "autoload": { From 0f4bcd99e6316be452030cd76b36cc0b581a3244 Mon Sep 17 00:00:00 2001 From: Kevin Pfeifer Date: Thu, 12 Sep 2024 09:53:20 +0200 Subject: [PATCH 2/6] #12 add error handling --- .phive/phars.xml | 4 +- composer.json | 1 + src/Command/ScheduleRunCommand.php | 23 ++- src/Error/SchedulerStoppedException.php | 10 + src/Scheduler/Scheduler.php | 5 + .../Command/ScheduleRunCommandTest.php | 172 +++++++++++++++--- 6 files changed, 184 insertions(+), 31 deletions(-) create mode 100644 src/Error/SchedulerStoppedException.php diff --git a/.phive/phars.xml b/.phive/phars.xml index bca1406..bca34b9 100644 --- a/.phive/phars.xml +++ b/.phive/phars.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/composer.json b/composer.json index 3b1f754..50d86c9 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ }, "require-dev": { "cakephp/cakephp-codesniffer": "^5.0", + "mockery/mockery": "^1.6", "phpunit/phpunit": "^10.5.5 || ^11.1.3" }, "license": "MIT", diff --git a/src/Command/ScheduleRunCommand.php b/src/Command/ScheduleRunCommand.php index e98417d..5aac625 100644 --- a/src/Command/ScheduleRunCommand.php +++ b/src/Command/ScheduleRunCommand.php @@ -6,8 +6,10 @@ use Cake\Command\Command; use Cake\Console\Arguments; use Cake\Console\ConsoleIo; +use CakeScheduler\Error\SchedulerStoppedException; use CakeScheduler\Scheduler\Event; use CakeScheduler\Scheduler\Scheduler; +use Throwable; class ScheduleRunCommand extends Command { @@ -34,7 +36,13 @@ public function execute(Arguments $args, ConsoleIo $io): int } $events->each(function (Event $event) use ($io): void { - $this->runEvent($event, $io); + $returnCode = $this->runEvent($event, $io); + + if ($returnCode === Scheduler::SHOULD_STOP_EXECUTION) { + $io->error('Error while executing command: ' . get_class($event->getCommand())); + + throw new SchedulerStoppedException('Scheduler was stopped by event listener'); + } }); return self::CODE_SUCCESS; @@ -48,8 +56,17 @@ public function execute(Arguments $args, ConsoleIo $io): int protected function runEvent(Event $event, ConsoleIo $io): ?int { $this->scheduler->dispatchEvent('CakeScheduler.beforeExecute', ['event' => $event]); - $result = $event->run($io); - $this->scheduler->dispatchEvent('CakeScheduler.afterExecute', ['event' => $event, 'result' => $result]); + try { + $result = $event->run($io); + $this->scheduler->dispatchEvent('CakeScheduler.afterExecute', ['event' => $event, 'result' => $result]); + } catch (Throwable $exception) { + $io->error($exception->getMessage()); + $event = $this->scheduler->dispatchEvent('CakeScheduler.errorExecute', [ + 'event' => $event, + 'exception' => $exception, + ]); + $result = $event->getResult(); + } return $result; } diff --git a/src/Error/SchedulerStoppedException.php b/src/Error/SchedulerStoppedException.php new file mode 100644 index 0000000..f635465 --- /dev/null +++ b/src/Error/SchedulerStoppedException.php @@ -0,0 +1,10 @@ +scheduler = Mockery::mock(Scheduler::class, [new Container()])->makePartial(); } public function testRunNoCommand(): void { $this->mockService(Scheduler::class, function () { - $schedulerMock = $this->getMockBuilder(Scheduler::class)->getMock(); - $collection = new Collection([]); + $this->scheduler->shouldReceive('dueEvents') + ->andReturn(new Collection([])); - $schedulerMock->expects($this->any()) - ->method('dueEvents') - ->willReturn($collection); - - return $schedulerMock; + return $this->scheduler; }); $this->exec('schedule:run'); @@ -47,16 +57,12 @@ public function testRunNoCommand(): void public function testRunSingleCommand(): void { $this->mockService(Scheduler::class, function () { - $schedulerMock = $this->getMockBuilder(Scheduler::class)->getMock(); - $event = new Event(new TestAppCommand(), []); $collection = new Collection([$event]); + $this->scheduler->shouldReceive('dueEvents') + ->andReturn($collection); - $schedulerMock->expects($this->any()) - ->method('dueEvents') - ->willReturn($collection); - - return $schedulerMock; + return $this->scheduler; }); $this->exec('schedule:run'); @@ -68,17 +74,14 @@ public function testRunSingleCommand(): void public function testRunMultipleCommands(): void { $this->mockService(Scheduler::class, function () { - $schedulerMock = $this->getMockBuilder(Scheduler::class)->getMock(); - $appEvent = new Event(new TestAppCommand(), []); $pluginEvent = new Event(new TestPluginCommand(), []); $collection = new Collection([$appEvent, $pluginEvent]); - $schedulerMock->expects($this->any()) - ->method('dueEvents') - ->willReturn($collection); + $this->scheduler->shouldReceive('dueEvents') + ->andReturn($collection); - return $schedulerMock; + return $this->scheduler; }); $this->exec('schedule:run'); @@ -92,16 +95,13 @@ public function testRunMultipleCommands(): void public function testRunSingleCommandWithArgsAndOptions(): void { $this->mockService(Scheduler::class, function () { - $schedulerMock = $this->getMockBuilder(Scheduler::class)->getMock(); - $event = new Event(new TestAppCommand(), ['somearg', '--myoption=someoption']); $collection = new Collection([$event]); - $schedulerMock->expects($this->any()) - ->method('dueEvents') - ->willReturn($collection); + $this->scheduler->shouldReceive('dueEvents') + ->andReturn($collection); - return $schedulerMock; + return $this->scheduler; }); $this->exec('schedule:run'); @@ -111,4 +111,124 @@ public function testRunSingleCommandWithArgsAndOptions(): void $this->assertOutputContains('with arg somearg'); $this->assertOutputContains('with option someoption'); } + + public function testRunSingleCommandWhichThrowsException(): void + { + $this->mockService(Scheduler::class, function () { + $command = new class () extends TestAppCommand { + public function execute(Arguments $args, ConsoleIo $io): void + { + throw new Exception('Test Exception'); + } + }; + + $event = new Event($command, []); + $collection = new Collection([$event]); + + $this->scheduler->shouldReceive('dueEvents') + ->andReturn($collection); + + return $this->scheduler; + }); + $this->exec('schedule:run'); + + $this->assertExitSuccess(); + $this->assertOutputContains('Executing [TestApp\\Command\\TestAppCommand@anonymous'); + $this->assertErrorContains('Test Exception'); + } + + public function testRunSingleCommandWhichThrowsExceptionAndListenerStopsExecution(): void + { + $this->mockService(Scheduler::class, function () { + $command = new class () extends TestAppCommand { + public function execute(Arguments $args, ConsoleIo $io): void + { + throw new Exception('Test Exception'); + } + }; + + $event = new Event($command, []); + $collection = new Collection([$event]); + + $this->scheduler->shouldReceive('dueEvents') + ->andReturn($collection); + + $this->scheduler->getEventManager()->on('CakeScheduler.errorExecute', function (EventInterface $event) { + $event->setResult(Scheduler::SHOULD_STOP_EXECUTION); + }); + + return $this->scheduler; + }); + + $this->expectException(SchedulerStoppedException::class); + $this->expectExceptionMessage('Scheduler was stopped by event listener'); + $this->exec('schedule:run'); + } + + public function testRunMultipleCommandsAndLastOneFails(): void + { + $this->mockService(Scheduler::class, function () { + $appEvent = new Event(new TestAppCommand(), []); + $pluginEvent = new Event(new TestPluginCommand(), []); + $failCommand = new class () extends TestAppCommand { + public function execute(Arguments $args, ConsoleIo $io): void + { + throw new Exception('Test Exception'); + } + }; + $failEvent = new Event($failCommand, []); + $collection = new Collection([$appEvent, $pluginEvent, $failEvent]); + + $this->scheduler->shouldReceive('dueEvents') + ->andReturn($collection); + + return $this->scheduler; + }); + $this->exec('schedule:run'); + + $this->assertExitSuccess(); + $this->assertOutputContains('Executing [TestApp\\Command\\TestAppCommand]'); + $this->assertOutputContains('Test App Command executed'); + $this->assertOutputContains('Executing [TestPlugin\\Command\\TestPluginCommand]'); + $this->assertOutputContains('Test Plugin Command executed'); + $this->assertOutputContains('Executing [TestApp\\Command\\TestAppCommand@anonymous'); + $this->assertErrorContains('Test Exception'); + } + + public function testRunMultipleCommandsAndSecondToLastOneFailsAndStopsExecution(): void + { + $this->mockService(Scheduler::class, function () { + $appEvent = new Event(new TestAppCommand(), []); + $pluginEvent = new Event(new TestPluginCommand(), []); + $failCommand = new class () extends TestAppCommand { + public function execute(Arguments $args, ConsoleIo $io): void + { + throw new Exception('Test Exception'); + } + }; + $failEvent = new Event($failCommand, []); + $collection = new Collection([$appEvent, $failEvent, $pluginEvent]); + + $this->scheduler->shouldReceive('dueEvents') + ->andReturn($collection); + + $this->scheduler->getEventManager()->on('CakeScheduler.errorExecute', function (EventInterface $event) { + $event->setResult(Scheduler::SHOULD_STOP_EXECUTION); + }); + + return $this->scheduler; + }); + + $this->expectException(SchedulerStoppedException::class); + $this->expectExceptionMessage('Scheduler was stopped by event listener'); + $this->exec('schedule:run'); + + $this->assertOutputContains('Executing [TestApp\\Command\\TestAppCommand]'); + $this->assertOutputContains('Test App Command executed'); + $this->assertOutputContains('Executing [TestApp\\Command\\TestAppCommand@anonymous'); + $this->assertErrorContains('Test Exception'); + + $this->assertOutputNotContains('Executing [TestPlugin\\Command\\TestPluginCommand]'); + $this->assertOutputNotContains('Test Plugin Command executed'); + } } From 05b783f238bfb8c0ed6451a5dbb62039b6f83c42 Mon Sep 17 00:00:00 2001 From: Kevin Pfeifer Date: Thu, 12 Sep 2024 17:05:48 +0200 Subject: [PATCH 3/6] #13 add callable support --- src/Scheduler/Scheduler.php | 47 ++++++++++++++++++- .../Command/ScheduleRunCommandTest.php | 20 ++++++++ tests/TestCase/Scheduler/SchedulerTest.php | 12 +++++ 3 files changed, 77 insertions(+), 2 deletions(-) diff --git a/src/Scheduler/Scheduler.php b/src/Scheduler/Scheduler.php index 635409d..73739d4 100644 --- a/src/Scheduler/Scheduler.php +++ b/src/Scheduler/Scheduler.php @@ -5,7 +5,10 @@ use Cake\Collection\Collection; use Cake\Collection\CollectionInterface; +use Cake\Command\Command; +use Cake\Console\Arguments; use Cake\Console\CommandInterface; +use Cake\Console\ConsoleIo; use Cake\Core\Container; use Cake\Core\ContainerInterface; use Cake\Event\EventDispatcherInterface; @@ -48,12 +51,16 @@ public function __construct(?Container $container = null) } /** - * @param string $command The FQCN of the command to be executed + * @param callable|string $command The FQCN of the command to be executed or a callable function * @param array $args Args which should be passed on to the command * @return \CakeScheduler\Scheduler\Event */ - public function execute(string $command, array $args = []): Event + public function execute(string|callable $command, array $args = []): Event { + if (is_callable($command)) { + return $this->addCallable($command, $args); + } + try { $commandObj = $this->container->get($command); } catch (ContainerExceptionInterface | NotFoundExceptionInterface $ex) { @@ -82,6 +89,42 @@ protected function addCommand(CommandInterface $command, array $args = []): Even return $event; } + /** + * @param callable $callable + * @param array $args + * @return \CakeScheduler\Scheduler\Event + */ + protected function addCallable(callable $callable, array $args = []): Event + { + $command = new class ($callable, $args) extends Command { + /** + * @param callable $callable + * @param array $args + */ + public function __construct( + protected $callable, + protected array $args = [] + ) { + } + + /** + * @param \Cake\Console\Arguments $args + * @param \Cake\Console\ConsoleIo $io + * @return int|null + */ + public function execute(Arguments $args, ConsoleIo $io): ?int + { + array_push($this->args, $io); + + return call_user_func_array($this->callable, $this->args); + } + }; + $event = new Event($command, $args); + $this->events = $this->events->appendItem($event); + + return $event; + } + /** * @return \Cake\Collection\CollectionInterface */ diff --git a/tests/TestCase/Command/ScheduleRunCommandTest.php b/tests/TestCase/Command/ScheduleRunCommandTest.php index 02c3918..4978f29 100644 --- a/tests/TestCase/Command/ScheduleRunCommandTest.php +++ b/tests/TestCase/Command/ScheduleRunCommandTest.php @@ -231,4 +231,24 @@ public function execute(Arguments $args, ConsoleIo $io): void $this->assertOutputNotContains('Executing [TestPlugin\\Command\\TestPluginCommand]'); $this->assertOutputNotContains('Test Plugin Command executed'); } + + public function testRunSingleCallable(): void + { + $this->mockService(Scheduler::class, function () { + $scheduler = new Scheduler(new Container()); + $scheduler->execute(function ($a, $b, $c, ConsoleIo $io) { + $io->info('Sum'); + + return $a + $b + $c; + }, [1,2,3]); + + return $scheduler; + }); + + $this->exec('schedule:run'); + + $this->assertExitSuccess(); + $this->assertOutputContains('Executing [Cake\\Command\\Command@anonymous'); + $this->assertOutputContains('Sum'); + } } diff --git a/tests/TestCase/Scheduler/SchedulerTest.php b/tests/TestCase/Scheduler/SchedulerTest.php index b7d8102..de3d584 100644 --- a/tests/TestCase/Scheduler/SchedulerTest.php +++ b/tests/TestCase/Scheduler/SchedulerTest.php @@ -6,6 +6,7 @@ use Cake\Chronos\Chronos; use Cake\Command\VersionCommand; use Cake\Console\Command\HelpCommand; +use Cake\Console\ConsoleIo; use Cake\Core\Container; use Cake\TestSuite\TestCase; use CakeScheduler\Scheduler\Scheduler; @@ -46,6 +47,17 @@ public function testAddMultipleDueEvents(): void Chronos::setTestNow('now'); } + public function testAddCallable(): void + { + $this->scheduler->execute(function ($a, $b, $c, ConsoleIo $io) { + $io->info('Sum'); + + return $a + $b + $c; + }, [1,2,3]); + $events = $this->scheduler->dueEvents(); + $this->assertNotEmpty($events); + } + public function testAddUnknownCommand(): void { $this->expectException(InvalidArgumentException::class); From 2a7eb1774126b2d1f79c9c581bba14ae0d905278 Mon Sep 17 00:00:00 2001 From: Kevin Pfeifer Date: Sun, 15 Sep 2024 09:05:16 +0200 Subject: [PATCH 4/6] adjust composer.json for CakePHP 5.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 50d86c9..b712fe5 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "type": "cakephp-plugin", "require": { "php": "^8.1", - "cakephp/cakephp": "dev-5.next", + "cakephp/cakephp": "^5.0", "dragonmantank/cron-expression": "^3.3" }, "require-dev": { From ce2feb80f147c517a5e3e36ea35d669ca8f00931 Mon Sep 17 00:00:00 2001 From: Kevin Pfeifer Date: Sun, 15 Sep 2024 09:09:38 +0200 Subject: [PATCH 5/6] remove not needed LogTestTrait --- tests/TestCase/Command/ScheduleRunCommandTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/TestCase/Command/ScheduleRunCommandTest.php b/tests/TestCase/Command/ScheduleRunCommandTest.php index 4978f29..e2b8e62 100644 --- a/tests/TestCase/Command/ScheduleRunCommandTest.php +++ b/tests/TestCase/Command/ScheduleRunCommandTest.php @@ -9,7 +9,6 @@ use Cake\Console\TestSuite\ConsoleIntegrationTestTrait; use Cake\Core\Container; use Cake\Event\EventInterface; -use Cake\TestSuite\LogTestTrait; use Cake\TestSuite\TestCase; use CakeScheduler\Error\SchedulerStoppedException; use CakeScheduler\Scheduler\Event; @@ -23,7 +22,6 @@ class ScheduleRunCommandTest extends TestCase { use ConsoleIntegrationTestTrait; - use LogTestTrait; protected Scheduler|LegacyMockInterface $scheduler; From 85d59046a4a0b251592f9b6eb2a373a7c44105ad Mon Sep 17 00:00:00 2001 From: Kevin Pfeifer Date: Sun, 17 Nov 2024 09:52:43 +0100 Subject: [PATCH 6/6] fix phpstan --- phpstan-baseline.neon | 19 +++++++++++++++++++ phpstan.neon | 3 +++ tests/TestCase/Scheduler/SchedulerTest.php | 2 +- 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 phpstan-baseline.neon diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..d9c2274 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,19 @@ +parameters: + ignoreErrors: + - + message: '#^Call to an undefined method CakeScheduler\\Scheduler\\Scheduler\|Mockery\\LegacyMockInterface\:\:getEventManager\(\)\.$#' + identifier: method.notFound + count: 2 + path: tests/TestCase/Command/ScheduleRunCommandTest.php + + - + message: '#^Call to an undefined method CakeScheduler\\Scheduler\\Scheduler\|Mockery\\LegacyMockInterface\:\:shouldReceive\(\)\.$#' + identifier: method.notFound + count: 8 + path: tests/TestCase/Command/ScheduleRunCommandTest.php + + - + message: '#^Call to an undefined method Mockery\\ExpectationInterface\|Mockery\\HigherOrderMessage\:\:andReturn\(\)\.$#' + identifier: method.notFound + count: 8 + path: tests/TestCase/Command/ScheduleRunCommandTest.php diff --git a/phpstan.neon b/phpstan.neon index ff6d291..cd4f009 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,3 +1,6 @@ +includes: + - phpstan-baseline.neon + parameters: paths: - src diff --git a/tests/TestCase/Scheduler/SchedulerTest.php b/tests/TestCase/Scheduler/SchedulerTest.php index ff20cea..95cfb23 100644 --- a/tests/TestCase/Scheduler/SchedulerTest.php +++ b/tests/TestCase/Scheduler/SchedulerTest.php @@ -55,7 +55,7 @@ public function testAddCallable(): void return $a + $b + $c; }, [1,2,3]); $events = $this->scheduler->dueEvents(); - $this->assertNotEmpty($events); + $this->assertFalse($events->isEmpty()); } public function testAddUnknownCommand(): void