diff --git a/CHANGELOG.md b/CHANGELOG.md index ce45b2c1..319828d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,15 @@ This option allow to run all tasks regardless of configured run time, part of issue [#11](https://github.com/lavary/crunz/issues/11) - PR [#120](https://github.com/lavary/crunz/pull/120) by [@PabloKowalczyk](https://github.com/PabloKowalczyk) +- `--task=TASK-NUMBER` option to `schedule:run` command. +This option allow to run only one specific task, +part of issue [#11](https://github.com/lavary/crunz/issues/11) - +PR [#129](https://github.com/lavary/crunz/pull/129) by [@PabloKowalczyk](https://github.com/PabloKowalczyk) + +### Changed +- Tasks in `schedule:list` is sorted by filename, +PR [#129](https://github.com/lavary/crunz/pull/129) by [@PabloKowalczyk](https://github.com/PabloKowalczyk) + ## 1.7.3 - 2018-06-15 ### Fixed diff --git a/config/services.xml b/config/services.xml index 472d73cd..d45e2593 100644 --- a/config/services.xml +++ b/config/services.xml @@ -14,6 +14,7 @@ + + + taskCollection = $taskCollection; $this->configuration = $configuration; $this->eventRunner = $eventRunner; $this->taskTimezone = $taskTimezone; + $this->scheduleFactory = $scheduleFactory; parent::__construct(); } @@ -70,6 +75,13 @@ protected function configure() InputOption::VALUE_NONE, 'Run all tasks regardless of configured run time.' ) + ->addOption( + 'task', + 't', + InputOption::VALUE_REQUIRED, + 'Which task to run. Provide task number from schedule:list command.', + null + ) ->setHelp('This command starts the Crunz event runner.'); } @@ -80,11 +92,12 @@ protected function execute(InputInterface $input, OutputInterface $output) { $this->arguments = $input->getArguments(); $this->options = $input->getOptions(); + $task = $this->options['task']; $files = $this->taskCollection ->all($this->arguments['source']) ; - if (!count($files)) { + if (!\count($files)) { $output->writeln('No task found! Please check your source path.'); return 0; @@ -92,7 +105,6 @@ protected function execute(InputInterface $input, OutputInterface $output) // List of schedules $schedules = []; - $tasksTimezone = $this->taskTimezone ->timezoneForComparisons() ; @@ -103,20 +115,39 @@ protected function execute(InputInterface $input, OutputInterface $output) continue; } - if (false === $this->options['force']) { - // We keep the events which are due and dismiss the rest. - $schedule->events( - $schedule->dueEvents( - $tasksTimezone - ) - ); - } - - if (count($schedule->events())) { + if (\count($schedule->events())) { $schedules[] = $schedule; } } + // Is specified task should be invoked? + if (null !== $task) { + $schedules = $this->scheduleFactory + ->singleTaskSchedule(TaskNumber::fromString($task), ...$schedules); + } + + $schedules = \array_map( + function (Schedule $schedule) use ($tasksTimezone) { + if (false === $this->options['force']) { + // We keep the events which are due and dismiss the rest. + $schedule->events( + $schedule->dueEvents( + $tasksTimezone + ) + ); + } + + return $schedule; + }, + $schedules + ); + $schedules = \array_filter( + $schedules, + function (Schedule $schedule) { + return \count($schedule->events()); + } + ); + if (!count($schedules)) { $output->writeln('No event is due!'); diff --git a/src/Exception/TaskNotExistException.php b/src/Exception/TaskNotExistException.php new file mode 100644 index 00000000..f8f88d5a --- /dev/null +++ b/src/Exception/TaskNotExistException.php @@ -0,0 +1,7 @@ +events(); + }, + $schedules + ); + + $flattenEvents = \array_merge(...$events); + + if (!isset($flattenEvents[$taskNumber->asArrayIndex()])) { + $tasksCount = \count($flattenEvents); + throw new TaskNotExistException("Task with id '{$taskNumber->asInt()}' not found. Last task id is '{$tasksCount}'."); + } + + $event = $flattenEvents[$taskNumber->asArrayIndex()]; + + $schedule = new Schedule(); + $schedule->events([$event]); + + return [$schedule]; + } +} diff --git a/src/Task/Collection.php b/src/Task/Collection.php index b5c86d4d..1fa1c3aa 100644 --- a/src/Task/Collection.php +++ b/src/Task/Collection.php @@ -37,6 +37,7 @@ public function all($source) $iterator = $this->finder ->files() ->name("*{$suffix}") + ->sortByName() ->in($source) ; diff --git a/src/Task/TaskNumber.php b/src/Task/TaskNumber.php new file mode 100644 index 00000000..b694e638 --- /dev/null +++ b/src/Task/TaskNumber.php @@ -0,0 +1,64 @@ +number = $number; + } + + /** + * @param $value string + * + * @return TaskNumber + * + * @throws WrongTaskNumberException + */ + public static function fromString($value) + { + if (!\is_string($value)) { + throw new WrongTaskNumberException('Passed task number is not string.'); + } + + if (!\is_numeric($value)) { + throw new WrongTaskNumberException("Task number '{$value}' is not numeric."); + } + + $number = (int) $value; + + return new self($number); + } + + /** + * @return int + */ + public function asInt() + { + return $this->number; + } + + /** + * @return int + */ + public function asArrayIndex() + { + return $this->number - 1; + } +} diff --git a/tests/Unit/Console/Command/ScheduleRunCommandTest.php b/tests/Unit/Console/Command/ScheduleRunCommandTest.php index 709d0ce6..f7edb198 100644 --- a/tests/Unit/Console/Command/ScheduleRunCommandTest.php +++ b/tests/Unit/Console/Command/ScheduleRunCommandTest.php @@ -4,6 +4,7 @@ use Crunz\Configuration\Configuration; use Crunz\Console\Command\ScheduleRunCommand; +use Crunz\Event; use Crunz\EventRunner; use Crunz\Schedule; use Crunz\Task\Collection; @@ -21,7 +22,7 @@ public function forceRunAllTasks() { $filename = $this->createTaskFile($this->taskContent()); - $mockInput = $this->mockInput(['force' => true]); + $mockInput = $this->mockInput(['force' => true, 'task' => null]); $mockOutput = $this->createMock(OutputInterface::class); $mockTaskCollection = $this->mockTaskCollection($filename); $mockEventRunner = $this->mockEventRunner($mockOutput); @@ -30,7 +31,8 @@ public function forceRunAllTasks() $mockTaskCollection, $this->createMock(Configuration::class), $mockEventRunner, - $this->createMock(Timezone::class) + $this->createMock(Timezone::class), + $this->createMock(Schedule\ScheduleFactory::class) ); $command->run( @@ -39,6 +41,48 @@ public function forceRunAllTasks() ); } + /** @test */ + public function runSpecificTask() + { + $filename1 = $this->createTaskFile($this->phpVersionTaskContent()); + $filename2 = $this->createTaskFile($this->phpVersionTaskContent()); + + $mockInput = $this->mockInput(['force' => false, 'task' => '1']); + $mockOutput = $this->createMock(OutputInterface::class); + $mockTaskCollection = $this->mockTaskCollection($filename1, $filename2); + $mockEventRunner = $this->mockEventRunner($mockOutput); + + $command = new ScheduleRunCommand( + $mockTaskCollection, + $this->createMock(Configuration::class), + $mockEventRunner, + $this->mockTimezoneProvider(), + $this->mockScheduleFactory() + ); + + $command->run( + $mockInput, + $mockOutput + ); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject|Schedule\ScheduleFactory + */ + private function mockScheduleFactory() + { + $mockEvent = $this->createMock(Event::class); + $mockSchedule = $this->createConfiguredMock(Schedule::class, ['events' => [$mockEvent]]); + $mockScheduleFactory = $this->createMock(Schedule\ScheduleFactory::class); + $mockScheduleFactory + ->expects($this->once()) + ->method('singleTaskSchedule') + ->willReturn([$mockSchedule]) + ; + + return $mockScheduleFactory; + } + private function mockEventRunner(OutputInterface $output) { $mockEventRunner = $this->createMock(EventRunner::class); @@ -56,7 +100,7 @@ function ($schedules) { return $isArray && 1 === $count && $schedule instanceof Schedule - ; + ; } ) ) @@ -70,28 +114,36 @@ private function mockInput(array $options) $mockInput = $this->createMock(InputInterface::class); $mockInput ->method('getOptions') - ->willReturn( - [ - 'force' => true, - ] - ) + ->willReturn($options) ; return $mockInput; } - private function mockTaskCollection($taskFile) + /** + * @return Timezone|\PHPUnit_Framework_MockObject_MockObject + */ + private function mockTimezoneProvider() + { + $timeZone = new \DateTimeZone('UTC'); + + return $this->createConfiguredMock(Timezone::class, ['timezoneForComparisons' => $timeZone]); + } + + private function mockTaskCollection(...$taskFiles) { - $mockFileInfo = $this->createMock(SplFileInfo::class); $mockTaskCollection = $this->createMock(Collection::class); + $mocksFileInfo = \array_map( + function ($taskFile) { + return $this->createConfiguredMock(SplFileInfo::class, ['getRealPath' => $taskFile]); + }, + $taskFiles + ); + $mockTaskCollection ->method('all') - ->willReturn([$mockFileInfo]) - ; - $mockFileInfo - ->method('getRealPath') - ->willReturn($taskFile) + ->willReturn($mocksFileInfo) ; return $mockTaskCollection; @@ -123,6 +175,24 @@ private function taskContent() ->skip(function () {return true;}) ; +return $schedule; +PHP; + } + + private function phpVersionTaskContent() + { + return <<<'PHP' +run('php -v') + ->everyMinute() + ->description('Show PHP version') +; + return $schedule; PHP; } diff --git a/tests/Unit/Schedule/ScheduleFactoryTest.php b/tests/Unit/Schedule/ScheduleFactoryTest.php new file mode 100644 index 00000000..20bf00be --- /dev/null +++ b/tests/Unit/Schedule/ScheduleFactoryTest.php @@ -0,0 +1,44 @@ +events([$event1, $event2]); + + $schedules = $factory->singleTaskSchedule(TaskNumber::fromString('1'), $schedule); + $firstSchedule = \reset($schedules); + + $this->assertSame([$event1], $firstSchedule->events()); + } + + /** @test */ + public function singleTaskScheduleThrowsExceptionOnWrongTaskNumber() + { + $factory = new ScheduleFactory(); + + $event1 = new Event(1, 'php -v'); + $schedule = new Schedule(); + $schedule->events([$event1]); + + $this->expectException(TaskNotExistException::class); + $this->expectExceptionMessage("Task with id '2' not found. Last task id is '1'."); + + $factory->singleTaskSchedule(TaskNumber::fromString('2'), $schedule); + } +} diff --git a/tests/Unit/Task/TaskNumberTest.php b/tests/Unit/Task/TaskNumberTest.php new file mode 100644 index 00000000..61187383 --- /dev/null +++ b/tests/Unit/Task/TaskNumberTest.php @@ -0,0 +1,80 @@ +expectException(WrongTaskNumberException::class); + $this->expectExceptionMessage('Passed task number is not string.'); + + TaskNumber::fromString($value); + } + + /** + * @test + * @dataProvider nonNumericProvider + */ + public function taskNumberCanNotBeNonNumericString($value) + { + $this->expectException(WrongTaskNumberException::class); + $this->expectExceptionMessage("Task number '{$value}' is not numeric."); + + TaskNumber::fromString($value); + } + + /** + * @test + * @dataProvider numericValueProvider + */ + public function taskNumberCanBeCreatedWithNumericStringValue($value, $expectedNumber) + { + $taskNumber = TaskNumber::fromString($value); + + $this->assertSame($expectedNumber, $taskNumber->asInt()); + } + + /** @test */ + public function arrayIndexIsOneStepLower() + { + $taskNumber = TaskNumber::fromString('14'); + + $this->assertSame(13, $taskNumber->asArrayIndex()); + } + + public function nonStringValueProvider() + { + yield 'null' => [null]; + yield 'float' => [3.14]; + yield 'int' => [7]; + yield 'array' => [[]]; + yield 'object' => [new \stdClass()]; + } + + public function numericValueProvider() + { + yield 'int' => [ + '155', + 155, + ]; + yield 'float' => [ + '3.14', + 3, + ]; + } + + public function nonNumericProvider() + { + yield 'chars' => ['abc']; + yield 'charsWithNumber' => ['1a2b3']; + } +}