diff --git a/.github/workflows/code-style.yml b/.github/workflows/code-style.yml index b8bcdcd5..aadc68cc 100644 --- a/.github/workflows/code-style.yml +++ b/.github/workflows/code-style.yml @@ -7,7 +7,7 @@ on: branches: - main schedule: - - cron: "0 0 * * *" + - cron: "5 0 * * *" jobs: php-cs-fixer: diff --git a/.github/workflows/infection.yml b/.github/workflows/infection.yml index 91e62836..d477ef14 100644 --- a/.github/workflows/infection.yml +++ b/.github/workflows/infection.yml @@ -7,7 +7,7 @@ on: branches: - main schedule: - - cron: "0 0 * * *" + - cron: "5 0 * * *" jobs: infection: diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index cb4fb0e7..cf18f3a5 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -7,7 +7,7 @@ on: branches: - main schedule: - - cron: "0 0 * * *" + - cron: "5 0 * * *" jobs: phpunit: diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml index 9a2f2fc3..557887ea 100644 --- a/.github/workflows/rector.yml +++ b/.github/workflows/rector.yml @@ -7,7 +7,7 @@ on: branches: - main schedule: - - cron: "0 0 * * *" + - cron: "5 0 * * *" jobs: phpstan: diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 0876add6..f236933f 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -7,7 +7,7 @@ on: branches: - main schedule: - - cron: "0 0 * * *" + - cron: "5 0 * * *" jobs: security: diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index f1d1a0b4..26e5a317 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -7,7 +7,7 @@ on: branches: - main schedule: - - cron: "0 0 * * *" + - cron: "5 0 * * *" jobs: phpstan: diff --git a/.gitignore b/.gitignore index 4fd8718f..0e93efa8 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ summary.log .php-cs-fixer.cache # Tests +tests/**/assets/ .phpunit.result.cache tests/.assets/**/*.json diff --git a/composer.json b/composer.json index 24cc8ee9..51b92d6f 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,7 @@ "SchedulerBundle\\Event\\": "src/Event/", "SchedulerBundle\\EventListener\\": "src/EventListener/", "SchedulerBundle\\Exception\\": "src/Exception/", + "SchedulerBundle\\Export\\": "src/Export/", "SchedulerBundle\\Expression\\": "src/Expression/", "SchedulerBundle\\Fiber\\": "src/Fiber/", "SchedulerBundle\\Messenger\\": "src/Messenger/", @@ -74,6 +75,7 @@ "Tests\\SchedulerBundle\\DependencyInjection\\Assets\\": "tests/DependencyInjection/Assets/", "Tests\\SchedulerBundle\\Event\\": "tests/Event/", "Tests\\SchedulerBundle\\EventListener\\": "tests/EventListener/", + "Tests\\SchedulerBundle\\Export\\": "tests/Export/", "Tests\\SchedulerBundle\\Expression\\": "tests/Expression/", "Tests\\SchedulerBundle\\Messenger\\": "tests/Messenger/", "Tests\\SchedulerBundle\\Middleware\\": "tests/Middleware/", diff --git a/src/Command/ExportCommand.php b/src/Command/ExportCommand.php new file mode 100644 index 00000000..c80a1c85 --- /dev/null +++ b/src/Command/ExportCommand.php @@ -0,0 +1,86 @@ + + */ +final class ExportCommand extends Command +{ + private ExporterRegistryInterface $exporterRegistry; + private SchedulerInterface $scheduler; + + protected static $defaultName = 'scheduler:export'; + + public function __construct( + ExporterRegistryInterface $exporterRegistry, + SchedulerInterface $scheduler + ) { + $this->exporterRegistry = $exporterRegistry; + $this->scheduler = $scheduler; + + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + protected function configure(): void + { + $this + ->setDescription('Export tasks to a specific format') + ->setDefinition([ + new InputArgument('format', InputArgument::REQUIRED, 'The format used to export tasks', 'crontab'), + new InputOption('filename', null, InputOption::VALUE_OPTIONAL, 'The name of the filename used to export tasks', '/etc/cron.d'), + ]) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $style = new SymfonyStyle($input, $output); + + try { + $tasks = $this->scheduler->getTasks(); + if (0 === $tasks->count()) { + $style->warning('No tasks found'); + + return Command::FAILURE; + } + + $filename = $input->getOption('filename'); + $exporter = $this->exporterRegistry->find($input->getArgument('format')); + + $tasks->walk(function (TaskInterface $task) use ($exporter, $filename): void { + $exporter->export($filename, $task); + }); + } catch (Throwable $throwable) { + $style->error([ + 'An error occurred when exporting tasks:', + $throwable->getMessage(), + ]); + + return Command::FAILURE; + } + + $style->success('The export has succeed'); + + return Command::SUCCESS; + } +} diff --git a/src/DependencyInjection/SchedulerBundleExtension.php b/src/DependencyInjection/SchedulerBundleExtension.php index 2a6c297b..2efd0723 100644 --- a/src/DependencyInjection/SchedulerBundleExtension.php +++ b/src/DependencyInjection/SchedulerBundleExtension.php @@ -17,6 +17,7 @@ use SchedulerBundle\Command\DebugProbeCommand; use SchedulerBundle\Command\ExecuteExternalProbeCommand; use SchedulerBundle\Command\ExecuteTaskCommand; +use SchedulerBundle\Command\ExportCommand; use SchedulerBundle\Command\ListFailedTasksCommand; use SchedulerBundle\Command\ListTasksCommand; use SchedulerBundle\Command\RebootSchedulerCommand; @@ -31,6 +32,10 @@ use SchedulerBundle\EventListener\TaskLoggerSubscriber; use SchedulerBundle\EventListener\TaskSubscriber; use SchedulerBundle\EventListener\WorkerLifecycleSubscriber; +use SchedulerBundle\Export\CronTabExporter; +use SchedulerBundle\Export\ExporterInterface; +use SchedulerBundle\Export\ExporterRegistry; +use SchedulerBundle\Export\ExporterRegistryInterface; use SchedulerBundle\Expression\BuilderInterface; use SchedulerBundle\Expression\ComputedExpressionBuilder; use SchedulerBundle\Expression\CronExpressionBuilder; @@ -185,6 +190,7 @@ final class SchedulerBundleExtension extends Extension private const EXECUTION_POLICY_TAG = 'scheduler.execution_policy'; private const WORKER_TAG = 'scheduler.worker'; private const SCHEDULER_MIDDLEWARE_TAG = 'scheduler.middleware'; + private const TASK_EXPORTER_TAG = 'scheduler.task_exporter'; public function load(array $configs, ContainerBuilder $container): void { @@ -208,6 +214,7 @@ public function load(array $configs, ContainerBuilder $container): void $this->registerExpressionFactoryAndPolicies($container); $this->registerBuilders($container); $this->registerRunners($container); + $this->registerExportTools($container); $this->registerNormalizer($container); $this->registerMessengerTools($container); $this->registerSubscribers($container); @@ -262,6 +269,7 @@ private function registerAutoConfigure(ContainerBuilder $container): void $container->registerForAutoconfiguration(TaskBagInterface::class)->addTag('scheduler.task_bag'); $container->registerForAutoconfiguration(SchedulerAwareInterface::class)->addTag('scheduler.entry_point'); $container->registerForAutoconfiguration(ExecutionPolicyInterface::class)->addTag(self::EXECUTION_POLICY_TAG); + $container->registerForAutoconfiguration(ExporterInterface::class)->addTag(self::TASK_EXPORTER_TAG); } private function registerConfigurationFactories(ContainerBuilder $container): void @@ -599,6 +607,17 @@ private function registerCommands(ContainerBuilder $container): void ]) ; + $container->register(ExportCommand::class, ExportCommand::class) + ->setArguments([ + new Reference(ExporterRegistryInterface::class, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE), + new Reference(SchedulerInterface::class, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE), + ]) + ->addTag('console.command') + ->addTag('container.preload', [ + 'class' => ExportCommand::class, + ]) + ; + $container->register(ListFailedTasksCommand::class, ListFailedTasksCommand::class) ->setArguments([ new Reference(WorkerInterface::class, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE), @@ -961,6 +980,29 @@ private function registerRunners(ContainerBuilder $container): void ; } + public function registerExportTools(ContainerBuilder $container): void + { + $container->register(ExporterRegistry::class, ExporterRegistry::class) + ->setArguments([ + new TaggedIteratorArgument(self::TASK_EXPORTER_TAG), + ]) + ->addTag('container.preload', [ + 'class' => ExporterRegistry::class, + ]) + ; + $container->setAlias(ExporterRegistryInterface::class, ExporterRegistry::class); + + $container->register(CronTabExporter::class, CronTabExporter::class) + ->setArguments([ + $container->getParameter('kernel.project_dir'), + ]) + ->addTag(self::TASK_EXPORTER_TAG) + ->addTag('container.preload', [ + 'class' => CronTabExporter::class, + ]) + ; + } + private function registerNormalizer(ContainerBuilder $container): void { $container->register(TaskNormalizer::class, TaskNormalizer::class) diff --git a/src/Export/AbstractExporter.php b/src/Export/AbstractExporter.php new file mode 100644 index 00000000..e1cf1d44 --- /dev/null +++ b/src/Export/AbstractExporter.php @@ -0,0 +1,23 @@ + + */ +abstract class AbstractExporter implements ExporterInterface +{ + private string $projectDir; + + public function __construct(string $projectDir) + { + $this->projectDir = $projectDir; + } + + protected function getProjectDir(): string + { + return $this->projectDir; + } +} diff --git a/src/Export/CronTabExporter.php b/src/Export/CronTabExporter.php new file mode 100644 index 00000000..076f82d0 --- /dev/null +++ b/src/Export/CronTabExporter.php @@ -0,0 +1,45 @@ + + */ +final class CronTabExporter extends AbstractExporter +{ + /** + * {@inheritdoc} + */ + public function export(string $filename, TaskInterface $task): void + { + $finalFilename = sprintf('%s/%s', $this->getProjectDir(), $task->getName()); + + $fs = new Filesystem(); + + if ($fs->exists($finalFilename)) { + return; + } + + $fs->touch($finalFilename); + $fs->dumpFile($finalFilename, sprintf( + '%s cd %s && php bin/console scheduler:execute --name %s', + $task->getExpression(), + $this->getProjectDir(), + $task->getName(), + )); + } + + /** + * {@inheritdoc} + */ + public function support(string $format): bool + { + return 'crontab' === $format; + } +} diff --git a/src/Export/ExporterInterface.php b/src/Export/ExporterInterface.php new file mode 100644 index 00000000..df36661f --- /dev/null +++ b/src/Export/ExporterInterface.php @@ -0,0 +1,20 @@ + + */ +interface ExporterInterface +{ + public function export(string $filename, TaskInterface $task): void; + + /** + * Determine if the exporter support the current @param string $format. + */ + public function support(string $format): bool; +} diff --git a/src/Export/ExporterRegistry.php b/src/Export/ExporterRegistry.php new file mode 100644 index 00000000..ec2c6322 --- /dev/null +++ b/src/Export/ExporterRegistry.php @@ -0,0 +1,79 @@ + + */ +final class ExporterRegistry implements ExporterRegistryInterface +{ + /** + * @var ExporterInterface[] + */ + private iterable $exporterList; + + /** + * @param ExporterInterface[] $exporterList + */ + public function __construct(iterable $exporterList) + { + $this->exporterList = $exporterList; + } + + /** + * {@inheritdoc} + */ + public function find(string $format): ExporterInterface + { + $filteredExporterList = $this->filter(static fn (ExporterInterface $exporter): bool => $exporter->support($format)); + + if (0 === $filteredExporterList->count()) { + throw new InvalidArgumentException(sprintf('No exporter found for the format "%s"', $format)); + } + + if (1 < $filteredExporterList->count()) { + throw new InvalidArgumentException('More than one exporter support this format, please consider using this exporter directly'); + } + + return $filteredExporterList->current(); + } + + /** + * {@inheritdoc} + */ + public function filter(Closure $func): self + { + return new self(array_filter($this->exporterList, $func, ARRAY_FILTER_USE_BOTH)); + } + + /** + * {@inheritdoc} + */ + public function current(): ExporterInterface + { + $exporter = current($this->exporterList); + if (false === $exporter) { + throw new RuntimeException('The current runner cannot be found'); + } + + return $exporter; + } + + /** + * {@inheritdoc} + */ + public function count(): int + { + return count($this->exporterList); + } +} diff --git a/src/Export/ExporterRegistryInterface.php b/src/Export/ExporterRegistryInterface.php new file mode 100644 index 00000000..8911b941 --- /dev/null +++ b/src/Export/ExporterRegistryInterface.php @@ -0,0 +1,26 @@ + + */ +interface ExporterRegistryInterface extends Countable +{ + /** + * Return a {@see ExporterInterface} using the @param string $format to determine which one can perform the export. + */ + public function find(string $format): ExporterInterface; + + public function filter(Closure $func): self; + + /** + * Return the currently available {@see ExporterInterface} + */ + public function current(): ExporterInterface; +} diff --git a/tests/Command/ExportCommandTest.php b/tests/Command/ExportCommandTest.php new file mode 100644 index 00000000..c859cac5 --- /dev/null +++ b/tests/Command/ExportCommandTest.php @@ -0,0 +1,14 @@ + + */ +final class ExportCommandTest extends TestCase +{ +} diff --git a/tests/DependencyInjection/SchedulerBundleExtensionTest.php b/tests/DependencyInjection/SchedulerBundleExtensionTest.php index a8cbee37..0aebf976 100644 --- a/tests/DependencyInjection/SchedulerBundleExtensionTest.php +++ b/tests/DependencyInjection/SchedulerBundleExtensionTest.php @@ -19,6 +19,7 @@ use SchedulerBundle\Command\DebugProbeCommand; use SchedulerBundle\Command\ExecuteExternalProbeCommand; use SchedulerBundle\Command\ExecuteTaskCommand; +use SchedulerBundle\Command\ExportCommand; use SchedulerBundle\Command\ListFailedTasksCommand; use SchedulerBundle\Command\ListTasksCommand; use SchedulerBundle\Command\RebootSchedulerCommand; @@ -35,6 +36,10 @@ use SchedulerBundle\EventListener\TaskLoggerSubscriber; use SchedulerBundle\EventListener\TaskSubscriber; use SchedulerBundle\EventListener\WorkerLifecycleSubscriber; +use SchedulerBundle\Export\CronTabExporter; +use SchedulerBundle\Export\ExporterInterface; +use SchedulerBundle\Export\ExporterRegistry; +use SchedulerBundle\Export\ExporterRegistryInterface; use SchedulerBundle\Expression\BuilderInterface; use SchedulerBundle\Expression\ComputedExpressionBuilder; use SchedulerBundle\Expression\CronExpressionBuilder; @@ -252,6 +257,8 @@ public function testInterfacesForAutoconfigureAreRegistered(): void self::assertTrue($autoconfigurationInterfaces[SchedulerAwareInterface::class]->hasTag('scheduler.entry_point')); self::assertArrayHasKey(ExecutionPolicyInterface::class, $autoconfigurationInterfaces); self::assertTrue($autoconfigurationInterfaces[ExecutionPolicyInterface::class]->hasTag('scheduler.execution_policy')); + self::assertArrayHasKey(ExporterInterface::class, $autoconfigurationInterfaces); + self::assertTrue($autoconfigurationInterfaces[ExporterInterface::class]->hasTag('scheduler.task_exporter')); } public function testConfigurationFactoriesAreRegistered(): void @@ -458,6 +465,7 @@ public function testTransportIsRegistered(): void $schedulerBundleExtension = new SchedulerBundleExtension(); $containerBuilder = new ContainerBuilder(); + $containerBuilder->setParameter('kernel.project_dir', 'foo'); $containerBuilder->register(SerializerInterface::class, SerializerInterface::class); $schedulerBundleExtension->load([ @@ -717,10 +725,24 @@ public function testCommandsAreRegistered(): void self::assertTrue($container->getDefinition(ExecuteTaskCommand::class)->hasTag('container.preload')); self::assertSame(ExecuteTaskCommand::class, $container->getDefinition(ExecuteTaskCommand::class)->getTag('container.preload')[0]['class']); + self::assertTrue($container->hasDefinition(ExportCommand::class)); + self::assertCount(2, $container->getDefinition(ExportCommand::class)->getArguments()); + self::assertInstanceOf(Reference::class, $container->getDefinition(ExportCommand::class)->getArgument(0)); + self::assertSame(ExporterRegistryInterface::class, (string) $container->getDefinition(ExportCommand::class)->getArgument(0)); + self::assertSame(ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, $container->getDefinition(ExportCommand::class)->getArgument(0)->getInvalidBehavior()); + self::assertInstanceOf(Reference::class, $container->getDefinition(ExportCommand::class)->getArgument(1)); + self::assertSame(SchedulerInterface::class, (string) $container->getDefinition(ExportCommand::class)->getArgument(1)); + self::assertSame(ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, $container->getDefinition(ExportCommand::class)->getArgument(1)->getInvalidBehavior()); + self::assertCount(2, $container->getDefinition(ExportCommand::class)->getTags()); + self::assertTrue($container->getDefinition(ExportCommand::class)->hasTag('console.command')); + self::assertTrue($container->getDefinition(ExportCommand::class)->hasTag('container.preload')); + self::assertSame(ExportCommand::class, $container->getDefinition(ExportCommand::class)->getTag('container.preload')[0]['class']); + self::assertTrue($container->hasDefinition(ListFailedTasksCommand::class)); self::assertCount(1, $container->getDefinition(ListFailedTasksCommand::class)->getArguments()); self::assertInstanceOf(Reference::class, $container->getDefinition(ListFailedTasksCommand::class)->getArgument(0)); self::assertSame(WorkerInterface::class, (string) $container->getDefinition(ListFailedTasksCommand::class)->getArgument(0)); + self::assertCount(2, $container->getDefinition(ListFailedTasksCommand::class)->getTags()); self::assertTrue($container->getDefinition(ListFailedTasksCommand::class)->hasTag('console.command')); self::assertTrue($container->getDefinition(ListFailedTasksCommand::class)->hasTag('container.preload')); self::assertSame(ListFailedTasksCommand::class, $container->getDefinition(ListFailedTasksCommand::class)->getTag('container.preload')[0]['class']); @@ -729,6 +751,7 @@ public function testCommandsAreRegistered(): void self::assertCount(1, $container->getDefinition(ListTasksCommand::class)->getArguments()); self::assertInstanceOf(Reference::class, $container->getDefinition(ListTasksCommand::class)->getArgument(0)); self::assertSame(SchedulerInterface::class, (string) $container->getDefinition(ListTasksCommand::class)->getArgument(0)); + self::assertCount(2, $container->getDefinition(ListTasksCommand::class)->getTags()); self::assertTrue($container->getDefinition(ListTasksCommand::class)->hasTag('console.command')); self::assertTrue($container->getDefinition(ListTasksCommand::class)->hasTag('container.preload')); self::assertSame(ListTasksCommand::class, $container->getDefinition(ListTasksCommand::class)->getTag('container.preload')[0]['class']); @@ -743,6 +766,7 @@ public function testCommandsAreRegistered(): void self::assertSame(EventDispatcherInterface::class, (string) $container->getDefinition(RebootSchedulerCommand::class)->getArgument(2)); self::assertInstanceOf(Reference::class, $container->getDefinition(RebootSchedulerCommand::class)->getArgument(3)); self::assertSame(LoggerInterface::class, (string) $container->getDefinition(RebootSchedulerCommand::class)->getArgument(3)); + self::assertCount(3, $container->getDefinition(RebootSchedulerCommand::class)->getTags()); self::assertTrue($container->getDefinition(RebootSchedulerCommand::class)->hasTag('console.command')); self::assertTrue($container->getDefinition(RebootSchedulerCommand::class)->hasTag('monolog.logger')); self::assertSame('scheduler', $container->getDefinition(RebootSchedulerCommand::class)->getTag('monolog.logger')[0]['channel']); @@ -1072,6 +1096,34 @@ public function testRunnersAreRegistered(): void self::assertSame(ChainedTaskRunner::class, $container->getDefinition(ChainedTaskRunner::class)->getTag('container.preload')[0]['class']); } + public function testExportToolsAreRegistered(): void + { + $container = $this->getContainer([ + 'path' => '/_foo', + 'timezone' => 'Europe/Paris', + 'transport' => [ + 'dsn' => 'memory://first_in_first_out', + ], + 'tasks' => [], + 'lock_store' => null, + ]); + + self::assertTrue($container->hasAlias(ExporterRegistryInterface::class)); + self::assertTrue($container->hasDefinition(ExporterRegistry::class)); + self::assertFalse($container->getDefinition(ExporterRegistry::class)->isPublic()); + self::assertCount(1, $container->getDefinition(ExporterRegistry::class)->getArguments()); + self::assertInstanceOf(TaggedIteratorArgument::class, $container->getDefinition(ExporterRegistry::class)->getArgument(0)); + self::assertTrue($container->getDefinition(ExporterRegistry::class)->hasTag('container.preload')); + self::assertSame(ExporterRegistry::class, $container->getDefinition(ExporterRegistry::class)->getTag('container.preload')[0]['class']); + + self::assertTrue($container->hasDefinition(CronTabExporter::class)); + self::assertCount(1, $container->getDefinition(CronTabExporter::class)->getArguments()); + self::assertSame('foo', $container->getDefinition(CronTabExporter::class)->getArgument(0)); + self::assertTrue($container->getDefinition(CronTabExporter::class)->hasTag('scheduler.task_exporter')); + self::assertTrue($container->getDefinition(CronTabExporter::class)->hasTag('container.preload')); + self::assertSame(CronTabExporter::class, $container->getDefinition(CronTabExporter::class)->getTag('container.preload')[0]['class']); + } + public function testNormalizersAreRegistered(): void { $container = $this->getContainer([ @@ -2265,6 +2317,7 @@ public function provideDoctrineDsn(): Generator private function getContainer(array $configuration = [], Closure $extraDefinitions = null, Closure $extraPasses = null): ContainerBuilder { $containerBuilder = new ContainerBuilder(); + $containerBuilder->setParameter('kernel.project_dir', 'foo'); $containerBuilder->registerExtension(new SchedulerBundleExtension()); $containerBuilder->loadFromExtension('scheduler_bundle', $configuration); diff --git a/tests/Export/CronTabExporterTest.php b/tests/Export/CronTabExporterTest.php new file mode 100644 index 00000000..6921857c --- /dev/null +++ b/tests/Export/CronTabExporterTest.php @@ -0,0 +1,58 @@ + + */ +final class CronTabExporterTest extends TestCase +{ + public function testExporterSupport(): void + { + $exporter = new CronTabExporter(__DIR__); + + self::assertFalse($exporter->support('cli')); + self::assertTrue($exporter->support('crontab')); + } + + public function testExporterCannotExportExistingFile(): void + { + $exporter = new CronTabExporter(__DIR__.'/assets'); + + self::assertFileExists(sprintf('%s/assets/foo', __DIR__)); + + $exporter->export('foo', new NullTask('foo')); + + self::assertFileExists(sprintf('%s/assets/foo', __DIR__)); + } + + public function testExporterCanExportUndefinedFile(): void + { + unlink(__DIR__.'/assets/bar'); + + $task = new NullTask('bar'); + + $exporter = new CronTabExporter(__DIR__.'/assets'); + + self::assertFileDoesNotExist(sprintf('%s/bar', __DIR__.'/assets')); + + $exporter->export('bar', $task); + + self::assertFileExists(sprintf('%s/bar', __DIR__.'/assets')); + self::assertSame(sprintf( + '%s cd %s && php bin/console scheduler:execute --name %s', + $task->getExpression(), + __DIR__.'/assets', + $task->getName(), + ), file_get_contents(__DIR__.'/assets/bar')); + } +} diff --git a/tests/Export/ExporterRegistryTest.php b/tests/Export/ExporterRegistryTest.php new file mode 100644 index 00000000..67ae33f8 --- /dev/null +++ b/tests/Export/ExporterRegistryTest.php @@ -0,0 +1,79 @@ + + */ +final class ExporterRegistryTest extends TestCase +{ + public function testRegistryCanReturnEmptyExporterList(): void + { + $registry = new ExporterRegistry([]); + + self::assertCount(0, $registry); + } + + public function testRegistryCanFilterExporterList(): void + { + $registry = new ExporterRegistry([ + new CronTabExporter(__DIR__), + ]); + + $registry->filter(static fn (ExporterInterface $exporter): bool => $exporter instanceof CronTabExporter); + self::assertCount(1, $registry); + } + + public function testRegistryCannotFindExporterWithEmptyList(): void + { + $registry = new ExporterRegistry([ + new CronTabExporter(__DIR__), + ]); + + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage('No exporter found for the format "foo"'); + self::expectExceptionCode(0); + $registry->find('foo'); + } + + public function testRegistryCannotFindExporterWithMultipleSupportingExporter(): void + { + $registry = new ExporterRegistry([ + new CronTabExporter(__DIR__), + new CronTabExporter(__DIR__), + ]); + + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage('More than one exporter support this format, please consider using this exporter directly'); + self::expectExceptionCode(0); + $registry->find('crontab'); + } + + public function testRegistryCanFindExporter(): void + { + $registry = new ExporterRegistry([ + new CronTabExporter(__DIR__), + ]); + + $exporter = $registry->find('crontab'); + self::assertInstanceOf(CronTabExporter::class, $exporter); + } + + public function testRegistryCanReturnCurrentExporter(): void + { + $registry = new ExporterRegistry([ + new CronTabExporter(__DIR__), + ]); + + $exporter = $registry->current(); + self::assertInstanceOf(CronTabExporter::class, $exporter); + } +} diff --git a/tests/Export/assets/.gitkeep b/tests/Export/assets/.gitkeep new file mode 100644 index 00000000..e69de29b