Skip to content

Commit

Permalink
#12 add error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
LordSimal committed Sep 12, 2024
1 parent 3d67cc7 commit 0f4bcd9
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 31 deletions.
4 changes: 2 additions & 2 deletions .phive/phars.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<phive xmlns="https://phar.io/phive">
<phar name="phpstan" version="1.10.21" installed="1.10.21" location="./tools/phpstan" copy="false"/>
<phar name="psalm" version="5.13.0" installed="5.13.0" location="./tools/psalm" copy="false"/>
<phar name="phpstan" version="1.12.3" installed="1.12.3" location="./tools/phpstan" copy="false"/>
<phar name="psalm" version="5.26.1" installed="5.26.1" location="./tools/psalm" copy="false"/>
</phive>
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
23 changes: 20 additions & 3 deletions src/Command/ScheduleRunCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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;
Expand All @@ -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;
}
Expand Down
10 changes: 10 additions & 0 deletions src/Error/SchedulerStoppedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);

namespace CakeScheduler\Error;

use RuntimeException;

class SchedulerStoppedException extends RuntimeException
{
}
5 changes: 5 additions & 0 deletions src/Scheduler/Scheduler.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ class Scheduler implements EventDispatcherInterface
*/
protected CollectionInterface $events;

/**
* The event code if execution should be stopped
*/
public const SHOULD_STOP_EXECUTION = 1;

/**
* @param \Cake\Core\Container|null $container The DI container instance from the app
*/
Expand Down
172 changes: 146 additions & 26 deletions tests/TestCase/Command/ScheduleRunCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,28 @@
namespace CakeScheduler\Test\TestCase\Command;

use Cake\Collection\Collection;
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
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;
use CakeScheduler\Scheduler\Scheduler;
use Exception;
use Mockery;
use Mockery\LegacyMockInterface;
use TestApp\Command\TestAppCommand;
use TestPlugin\Command\TestPluginCommand;

class ScheduleRunCommandTest extends TestCase
{
use ConsoleIntegrationTestTrait;
use LogTestTrait;

protected Scheduler|LegacyMockInterface $scheduler;

protected function setUp(): void
{
Expand All @@ -24,19 +36,17 @@ protected function setUp(): void
'TestApp\Application',
[PLUGIN_TESTS . 'test_app' . DS . 'config']
);

$this->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');

Expand All @@ -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');

Expand All @@ -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');

Expand All @@ -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');

Expand All @@ -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');
}
}

0 comments on commit 0f4bcd9

Please sign in to comment.