From 0f4bcd99e6316be452030cd76b36cc0b581a3244 Mon Sep 17 00:00:00 2001 From: Kevin Pfeifer Date: Thu, 12 Sep 2024 09:53:20 +0200 Subject: [PATCH] #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'); + } }