From 52e5eb8681a48caa6fe98c3242ae7b79d8379945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Barto=C5=A1?= Date: Sun, 10 Mar 2024 18:33:00 +0100 Subject: [PATCH] ExplainCommand: --id option --- docs/README.md | 11 +- src/Command/ExplainCommand.php | 89 ++++++++++++++++ tests/Unit/Command/ExplainCommandTest.php | 121 +++++++++++++++++++++- tools/phpstan.baseline.neon | 5 + 4 files changed, 221 insertions(+), 5 deletions(-) diff --git a/docs/README.md b/docs/README.md index 4d9607c..58094a5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -587,7 +587,7 @@ use Orisai\Scheduler\Command\WorkerCommand; $app = new Application(); $app->addCommands([ - new ExplainCommand(), + new ExplainCommand($scheduler), new ListCommand($scheduler), new RunCommand($scheduler), new RunJobCommand($scheduler), @@ -630,6 +630,7 @@ List all scheduled jobs (in `expression / second (timezone) [id] name... next-du - use `--timezone` (or `-tz`) to display times in specified timezone instead of one used by application - e.g. `--tz=UTC` - use `--explain` to explain whole expression, including [seconds](#seconds) and [timezones](#timezones) + - [Explain command](#explain-command) with `--id` parameter can be used to explain specific job ### Worker command @@ -646,7 +647,13 @@ Run scheduler repeatedly, once every minute Explain cron expression syntax -`bin/console scheduler:explain` +```shell +bin/console scheduler:explain +bin/console scheduler:explain --id="job id" +``` + +- use `--id=` option to explain specific job + - [List command](#list-command) with `--explain` parameter can be used to explain all jobs ## Lazy loading diff --git a/src/Command/ExplainCommand.php b/src/Command/ExplainCommand.php index b07b468..d49fefb 100644 --- a/src/Command/ExplainCommand.php +++ b/src/Command/ExplainCommand.php @@ -2,13 +2,41 @@ namespace Orisai\Scheduler\Command; +use DateTimeZone; +use Orisai\Clock\SystemClock; +use Orisai\CronExpressionExplainer\CronExpressionExplainer; +use Orisai\CronExpressionExplainer\DefaultCronExpressionExplainer; +use Orisai\Scheduler\Job\JobSchedule; +use Orisai\Scheduler\Scheduler; +use Psr\Clock\ClockInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use function assert; +use function is_string; final class ExplainCommand extends Command { + private Scheduler $scheduler; + + private CronExpressionExplainer $explainer; + + private ClockInterface $clock; + + public function __construct( + Scheduler $scheduler, + ?CronExpressionExplainer $explainer = null, + ?ClockInterface $clock = null + ) + { + parent::__construct(); + $this->scheduler = $scheduler; + $this->explainer = $explainer ?? new DefaultCronExpressionExplainer(); + $this->clock = $clock ?? new SystemClock(); + } + public static function getDefaultName(): string { return 'scheduler:explain'; @@ -19,13 +47,74 @@ public static function getDefaultDescription(): string return 'Explain cron expression'; } + protected function configure(): void + { + /** @infection-ignore-all */ + parent::configure(); + $this->addOption('id', null, InputOption::VALUE_REQUIRED, 'ID of job to explain'); + } + protected function execute(InputInterface $input, OutputInterface $output): int { + $id = $this->validateIdOption($input); + + if ($id !== null) { + return $this->explainJobWithId($id, $output); + } + $this->explainSyntax($output); return 0; } + private function validateIdOption(InputInterface $input): ?string + { + $id = $input->getOption('id'); + assert(is_string($id) || $id === null); + + return $id; + } + + private function explainJobWithId(string $id, OutputInterface $output): int + { + $jobSchedules = $this->scheduler->getJobSchedules(); + $jobSchedule = $jobSchedules[$id] ?? null; + + if ($jobSchedule === null) { + $output->writeln("Job with id '$id' does not exist."); + + return 1; + } + + $output->writeln($this->explainer->explain( + $jobSchedule->getExpression()->getExpression(), + $jobSchedule->getRepeatAfterSeconds(), + $this->computeTimeZone($jobSchedule, $this->clock->now()->getTimezone()), + )); + + return 0; + } + + private function computeTimeZone(JobSchedule $jobSchedule, DateTimeZone $renderedTimeZone): ?DateTimeZone + { + $timeZone = $jobSchedule->getTimeZone(); + $clockTimeZone = $this->clock->now()->getTimezone(); + + if ($timeZone === null && $renderedTimeZone->getName() !== $clockTimeZone->getName()) { + $timeZone = $clockTimeZone; + } + + if ($timeZone === null) { + return null; + } + + if ($timeZone->getName() === $renderedTimeZone->getName()) { + return null; + } + + return $timeZone; + } + private function explainSyntax(OutputInterface $output): void { $output->writeln( diff --git a/tests/Unit/Command/ExplainCommandTest.php b/tests/Unit/Command/ExplainCommandTest.php index b060d1f..991bac9 100644 --- a/tests/Unit/Command/ExplainCommandTest.php +++ b/tests/Unit/Command/ExplainCommandTest.php @@ -2,13 +2,19 @@ namespace Tests\Orisai\Scheduler\Unit\Command; +use Closure; +use Cron\CronExpression; +use DateTimeZone; +use Orisai\Clock\FrozenClock; use Orisai\Scheduler\Command\ExplainCommand; +use Orisai\Scheduler\Job\CallbackJob; +use Orisai\Scheduler\SimpleScheduler; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; +use Tests\Orisai\Scheduler\Doubles\CallbackList; use function array_map; use function explode; use function implode; -use function putenv; use function rtrim; use const PHP_EOL; @@ -17,10 +23,12 @@ final class ExplainCommandTest extends TestCase public function testBasicExplain(): void { - $command = new ExplainCommand(); + $clock = new FrozenClock(1, new DateTimeZone('Europe/Prague')); + $scheduler = new SimpleScheduler(null, null, null, $clock); + + $command = new ExplainCommand($scheduler); $tester = new CommandTester($command); - putenv('COLUMNS=80'); $code = $tester->execute([]); self::assertSame( @@ -62,6 +70,113 @@ public function testBasicExplain(): void - seconds - repeat job every n seconds - timezone - run only when cron expression matches within given timezone +MSG, + implode( + PHP_EOL, + array_map( + static fn (string $s): string => rtrim($s), + explode(PHP_EOL, $tester->getDisplay()), + ), + ), + ); + self::assertSame($command::SUCCESS, $code); + } + + public function testExplainId(): void + { + $clock = new FrozenClock(1, new DateTimeZone('Europe/Prague')); + $scheduler = new SimpleScheduler(null, null, null, $clock); + + $cbs = new CallbackList(); + $scheduler->addJob( + new CallbackJob(Closure::fromCallable([$cbs, 'job1'])), + new CronExpression('* * * * *'), + 'one', + 0, + new DateTimeZone('Europe/Prague'), + ); + $scheduler->addJob( + new CallbackJob(Closure::fromCallable([$cbs, 'job1'])), + new CronExpression('*/30 7-15 * * 1-5'), + 'two', + 0, + new DateTimeZone('America/New_York'), + ); + $scheduler->addJob( + new CallbackJob(Closure::fromCallable($cbs)), + new CronExpression('* * * 4 *'), + 'three', + 10, + ); + + $command = new ExplainCommand($scheduler, null, $clock); + $tester = new CommandTester($command); + + $code = $tester->execute([ + '--id' => 'non-existent', + ]); + + self::assertSame( + <<<'MSG' +Job with id 'non-existent' does not exist. + +MSG, + implode( + PHP_EOL, + array_map( + static fn (string $s): string => rtrim($s), + explode(PHP_EOL, $tester->getDisplay()), + ), + ), + ); + self::assertSame($command::FAILURE, $code); + + $code = $tester->execute([ + '--id' => 'one', + ]); + + self::assertSame( + <<<'MSG' +At every minute. + +MSG, + implode( + PHP_EOL, + array_map( + static fn (string $s): string => rtrim($s), + explode(PHP_EOL, $tester->getDisplay()), + ), + ), + ); + self::assertSame($command::SUCCESS, $code); + + $code = $tester->execute([ + '--id' => 'two', + ]); + + self::assertSame( + <<<'MSG' +At every 30th minute past every hour from 7 through 15 on every day-of-week from Monday through Friday in America/New_York time zone. + +MSG, + implode( + PHP_EOL, + array_map( + static fn (string $s): string => rtrim($s), + explode(PHP_EOL, $tester->getDisplay()), + ), + ), + ); + self::assertSame($command::SUCCESS, $code); + + $code = $tester->execute([ + '--id' => 'three', + ]); + + self::assertSame( + <<<'MSG' +At every 10 seconds in April. + MSG, implode( PHP_EOL, diff --git a/tools/phpstan.baseline.neon b/tools/phpstan.baseline.neon index bc45541..b31903e 100644 --- a/tools/phpstan.baseline.neon +++ b/tools/phpstan.baseline.neon @@ -1,5 +1,10 @@ parameters: ignoreErrors: + - + message: "#^Parameter \\#1 \\$expression of method Orisai\\\\CronExpressionExplainer\\\\CronExpressionExplainer\\:\\:explain\\(\\) expects string, string\\|null given\\.$#" + count: 1 + path: ../src/Command/ExplainCommand.php + - message: "#^Parameter \\#1 \\$expression of method Orisai\\\\CronExpressionExplainer\\\\CronExpressionExplainer\\:\\:explain\\(\\) expects string, string\\|null given\\.$#" count: 1