Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#12 add error handling #14

Merged
merged 1 commit into from
Sep 12, 2024
Merged
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
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
@@ -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",
23 changes: 20 additions & 3 deletions src/Command/ScheduleRunCommand.php
Original file line number Diff line number Diff line change
@@ -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;
}
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
@@ -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
*/
172 changes: 146 additions & 26 deletions tests/TestCase/Command/ScheduleRunCommandTest.php
Original file line number Diff line number Diff line change
@@ -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
{
@@ -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');

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