From daf5eaf311e5189402799d416380840c28c94094 Mon Sep 17 00:00:00 2001 From: Jakub Zalas Date: Sun, 2 Sep 2018 14:14:23 +0100 Subject: [PATCH 1/3] Replace container paramters with runtime services --- src/Cli/Application.php | 11 +++---- src/Cli/ServiceContainer.php | 50 ++++++++++++++++-------------- tests/Cli/ServiceContainerTest.php | 43 ++++++++++++++++++------- 3 files changed, 63 insertions(+), 41 deletions(-) diff --git a/src/Cli/Application.php b/src/Cli/Application.php index 027311a8..d37d13f4 100644 --- a/src/Cli/Application.php +++ b/src/Cli/Application.php @@ -2,6 +2,7 @@ namespace Zalas\Toolbox\Cli; +use Psr\Container\ContainerInterface; use Symfony\Component\Console\Application as CliApplication; use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; use Symfony\Component\Console\CommandLoader\ContainerCommandLoader; @@ -22,14 +23,12 @@ public function __construct(string $version, ServiceContainer $serviceContainer) $this->serviceContainer = $serviceContainer; - $this->setCommandLoader($this->createCommandLoader()); + $this->setCommandLoader($this->createCommandLoader($serviceContainer)); } public function doRun(InputInterface $input, OutputInterface $output) { - $this->serviceContainer->setParameter('toolbox_json', function () use ($input): array { - return $input->getOption('tools'); - }); + $this->serviceContainer->set(InputInterface::class, $input); return parent::doRun($input, $output); } @@ -49,10 +48,10 @@ private function toolsJsonDefault(): array : [__DIR__.'/../../resources/pre-installation.json', __DIR__.'/../../resources/tools.json']; } - private function createCommandLoader(): CommandLoaderInterface + private function createCommandLoader(ContainerInterface $container): CommandLoaderInterface { return new ContainerCommandLoader( - $this->serviceContainer, + $container, [ InstallCommand::NAME => InstallCommand::class, ListCommand::NAME => ListCommand::class, diff --git a/src/Cli/ServiceContainer.php b/src/Cli/ServiceContainer.php index d11e55da..c0ec5da5 100644 --- a/src/Cli/ServiceContainer.php +++ b/src/Cli/ServiceContainer.php @@ -6,6 +6,7 @@ use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; use RuntimeException; +use Symfony\Component\Console\Input\InputInterface; use Zalas\Toolbox\Cli\Command\InstallCommand; use Zalas\Toolbox\Cli\Command\ListCommand; use Zalas\Toolbox\Cli\Command\TestCommand; @@ -19,8 +20,6 @@ class ServiceContainer implements ContainerInterface { - private $parameters; - private $services = [ InstallCommand::class => 'createInstallCommand', ListCommand::class => 'createListCommand', @@ -32,40 +31,43 @@ class ServiceContainer implements ContainerInterface Tools::class => 'createTools', ]; - /** - * {@inheritdoc} - */ - public function get($id) + private $runtimeServices = [ + InputInterface::class => null, + ]; + + public function set(string $id, /*object */$service): void { - if (!$this->has($id)) { - throw new class(\sprintf('The "%s" service is not registered in the service container.', $id)) extends \RuntimeException implements NotFoundExceptionInterface { + if (!\array_key_exists($id, $this->runtimeServices)) { + throw new class(\sprintf('The "%s" runtime service is not expected.', $id)) extends RuntimeException implements ContainerExceptionInterface { }; } - return \call_user_func([$this, $this->services[$id]]); + $this->runtimeServices[$id] = $service; } /** * {@inheritdoc} */ - public function has($id) + public function get($id) { - return \in_array($id, \array_keys($this->services)); - } + if (isset($this->runtimeServices[$id])) { + return $this->runtimeServices[$id]; + } - public function setParameter(string $name, $value): void - { - $this->parameters[$name] = $value; + if (isset($this->services[$id])) { + return \call_user_func([$this, $this->services[$id]]); + } + + throw new class(\sprintf('The "%s" service is not registered in the service container.', $id)) extends RuntimeException implements NotFoundExceptionInterface { + }; } - private function getParameter(string $name) + /** + * {@inheritdoc} + */ + public function has($id) { - if (!isset($this->parameters[$name])) { - throw new class(\sprintf('The "%s" parameter is not defined.', $name)) extends RuntimeException implements ContainerExceptionInterface { - }; - } - - return $this->parameters[$name]; + return isset($this->services[$id]) || isset($this->runtimeServices[$id]); } private function createInstallCommand(): InstallCommand @@ -105,6 +107,8 @@ private function createTestToolsUseCase(): TestTools private function createTools(): Tools { - return new JsonTools($this->getParameter('toolbox_json')); + return new JsonTools(function (): array { + return $this->get(InputInterface::class)->getOption('tools'); + }); } } diff --git a/tests/Cli/ServiceContainerTest.php b/tests/Cli/ServiceContainerTest.php index 09be8300..2206d79c 100644 --- a/tests/Cli/ServiceContainerTest.php +++ b/tests/Cli/ServiceContainerTest.php @@ -6,6 +6,7 @@ use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; +use Symfony\Component\Console\Input\InputInterface; use Zalas\Toolbox\Cli\Command\InstallCommand; use Zalas\Toolbox\Cli\Command\ListCommand; use Zalas\Toolbox\Cli\Command\TestCommand; @@ -21,9 +22,6 @@ class ServiceContainerTest extends TestCase protected function setUp() { $this->container = new ServiceContainer(); - $this->container->setParameter('toolbox_json', function () { - return [__DIR__.'/../resources/tools.json']; - }); } public function test_it_is_a_psr_container() @@ -44,15 +42,6 @@ public function test_it_throws_an_exception_if_unregistered_service_is_accessed( $this->container->get('foo'); } - public function test_it_throws_an_exception_if_toolbox_json_parameter_is_missing() - { - $this->expectException(ContainerExceptionInterface::class); - $this->expectExceptionMessage('The "toolbox_json" parameter is not defined.'); - - $this->container = new ServiceContainer(); - $this->container->get(InstallCommand::class); - } - public function test_it_creates_the_install_command() { $this->assertTrue($this->container->has(InstallCommand::class)); @@ -70,4 +59,34 @@ public function test_it_creates_the_test_command() $this->assertTrue($this->container->has(TestCommand::class)); $this->assertInstanceOf(TestCommand::class, $this->container->get(TestCommand::class)); } + + public function test_it_registers_a_runtime_service() + { + $service = $this->prophesize(InputInterface::class)->reveal(); + + $this->container->set(InputInterface::class, $service); + + $this->assertTrue($this->container->has(InputInterface::class)); + $this->assertSame($service, $this->container->get(InputInterface::class)); + } + + public function test_it_returns_false_if_runtime_service_has_not_been_defined() + { + $this->assertFalse($this->container->has(InputInterface::class)); + } + + public function test_it_throws_an_exception_if_missing_runtime_service_is_accessed() + { + $this->expectException(NotFoundExceptionInterface::class); + + $this->container->get(InputInterface::class); + } + + public function test_it_throws_an_exception_if_unknown_runtime_service_is_provided() + { + $this->expectException(ContainerExceptionInterface::class); + $this->expectExceptionMessage('The "foo" runtime service is not expected.'); + + $this->container->set('foo', new \stdClass()); + } } From 22ed71e6d7edd6534232131e8b98d7e7b8ff6d7c Mon Sep 17 00:00:00 2001 From: Jakub Zalas Date: Mon, 3 Sep 2018 19:14:23 +0100 Subject: [PATCH 2/3] Add a missing test for reading tools location from the environment --- composer.json | 3 ++- phpunit.xml.dist | 4 ++++ tests/Cli/ApplicationTest.php | 8 ++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index c17124d3..2dcaf49b 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,8 @@ "psr/container": "^1.0" }, "require-dev": { - "phpunit/phpunit": "^7.3" + "phpunit/phpunit": "^7.3", + "zalas/phpunit-globals": "^1.1" }, "autoload": { "psr-4": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index d0528e28..461c6568 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -19,6 +19,10 @@ + + + + diff --git a/tests/Cli/ApplicationTest.php b/tests/Cli/ApplicationTest.php index b8919f18..5b1ce8f3 100644 --- a/tests/Cli/ApplicationTest.php +++ b/tests/Cli/ApplicationTest.php @@ -47,6 +47,14 @@ public function test_it_defines_tools_option() $this->assertTrue($this->app->getDefinition()->hasOption('tools')); } + /** + * @putenv TOOLBOX_JSON=resources/pre.json,resources/tools.json + */ + public function test_it_takes_the_tools_option_default_from_environment_if_present() + { + $this->assertSame(['resources/pre.json', 'resources/tools.json'], $this->app->getDefinition()->getOption('tools')->getDefault()); + } + /** * @group integration */ From 734fc1db9b7b1e76f821c6229719434aaee284b6 Mon Sep 17 00:00:00 2001 From: Jakub Zalas Date: Mon, 3 Sep 2018 20:30:03 +0100 Subject: [PATCH 3/3] Introduce the --dry-run option to install and test commands --- Makefile | 4 +- README.md | 16 +++ src/Cli/Application.php | 1 + src/Cli/Command/InstallCommand.php | 2 + src/Cli/Command/TestCommand.php | 2 + src/Cli/Runner/DryRunner.php | 24 ++++ src/Cli/Runner/LazyRunner.php | 63 +++++++++ src/Cli/ServiceContainer.php | 16 +++ tests/Cli/ApplicationTest.php | 31 ++++- tests/Cli/Command/InstallCommandTest.php | 5 + tests/Cli/Command/TestCommandTest.php | 5 + tests/Cli/Command/ToolboxCommandTestCase.php | 6 +- tests/Cli/Runner/DryRunnerTest.php | 48 +++++++ tests/Cli/Runner/LazyRunnerTest.php | 137 +++++++++++++++++++ tests/Cli/ServiceContainerTest.php | 43 +++--- 15 files changed, 379 insertions(+), 24 deletions(-) create mode 100644 src/Cli/Runner/DryRunner.php create mode 100644 src/Cli/Runner/LazyRunner.php create mode 100644 tests/Cli/Runner/DryRunnerTest.php create mode 100644 tests/Cli/Runner/LazyRunnerTest.php diff --git a/Makefile b/Makefile index cc2f9fd9..8cf92810 100644 --- a/Makefile +++ b/Makefile @@ -104,10 +104,10 @@ tools/deptrac: curl -Ls http://get.sensiolabs.de/deptrac.phar -o tools/deptrac && chmod +x tools/deptrac tools/infection: tools/infection.pubkey - curl -Ls https://github.com/infection/infection/releases/download/0.10.1/infection.phar -o tools/infection && chmod +x tools/infection + curl -Ls https://github.com/infection/infection/releases/download/0.10.3/infection.phar -o tools/infection && chmod +x tools/infection tools/infection.pubkey: - curl -Ls https://github.com/infection/infection/releases/download/0.10.1/infection.phar.pubkey -o tools/infection.pubkey + curl -Ls https://github.com/infection/infection/releases/download/0.10.3/infection.phar.pubkey -o tools/infection.pubkey tools/box: curl -Ls https://github.com/humbug/box/releases/download/3.0.0-RC.0/box.phar -o tools/box && chmod +x tools/box diff --git a/README.md b/README.md index 0044159a..d0c23cbf 100644 --- a/README.md +++ b/README.md @@ -39,12 +39,28 @@ curl -s https://api.github.com/repos/jakzal/toolbox/releases/latest \ ./toolbox install ``` +#### Dry run + +To only see what commands would be executed, use the dry run mode: + +``` +./toolbox install --dry-run +``` + ### Test if installed tools are usable ``` ./toolbox test ``` +#### Dry run + +To only see what commands would be executed, use the dry run mode: + +``` +./toolbox test --dry-run +``` + ### Tools definitions By default `resources/pre-installation.json` and `resources/tools.json` are used to load tool definitions. diff --git a/src/Cli/Application.php b/src/Cli/Application.php index d37d13f4..5a7c8307 100644 --- a/src/Cli/Application.php +++ b/src/Cli/Application.php @@ -29,6 +29,7 @@ public function __construct(string $version, ServiceContainer $serviceContainer) public function doRun(InputInterface $input, OutputInterface $output) { $this->serviceContainer->set(InputInterface::class, $input); + $this->serviceContainer->set(OutputInterface::class, $output); return parent::doRun($input, $output); } diff --git a/src/Cli/Command/InstallCommand.php b/src/Cli/Command/InstallCommand.php index 44beb937..406b995f 100644 --- a/src/Cli/Command/InstallCommand.php +++ b/src/Cli/Command/InstallCommand.php @@ -4,6 +4,7 @@ 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 Zalas\Toolbox\Runner\Runner; use Zalas\Toolbox\UseCase\InstallTools; @@ -26,6 +27,7 @@ public function __construct(InstallTools $useCase, Runner $runner) protected function configure() { $this->setDescription('Installs tools'); + $this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Output the command without executing it'); } protected function execute(InputInterface $input, OutputInterface $output) diff --git a/src/Cli/Command/TestCommand.php b/src/Cli/Command/TestCommand.php index 30db9830..0dd19e3b 100644 --- a/src/Cli/Command/TestCommand.php +++ b/src/Cli/Command/TestCommand.php @@ -4,6 +4,7 @@ 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 Zalas\Toolbox\Runner\Runner; use Zalas\Toolbox\UseCase\TestTools; @@ -26,6 +27,7 @@ public function __construct(TestTools $useCase, Runner $runner) protected function configure() { $this->setDescription('Runs basic tests to verify tools are installed'); + $this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Output the command without executing it'); } protected function execute(InputInterface $input, OutputInterface $output) diff --git a/src/Cli/Runner/DryRunner.php b/src/Cli/Runner/DryRunner.php new file mode 100644 index 00000000..244ca609 --- /dev/null +++ b/src/Cli/Runner/DryRunner.php @@ -0,0 +1,24 @@ +output = $output; + } + + public function run(Command $command): int + { + $this->output->writeln((string) $command); + + return 0; + } +} diff --git a/src/Cli/Runner/LazyRunner.php b/src/Cli/Runner/LazyRunner.php new file mode 100644 index 00000000..abf2fdcc --- /dev/null +++ b/src/Cli/Runner/LazyRunner.php @@ -0,0 +1,63 @@ +guardServiceAvailability($container); + + $this->container = $container; + } + + public function run(Command $command): int + { + return $this->runner()->run($command); + } + + private function runner(): Runner + { + if (null === $this->runner) { + $this->runner = $this->initializeRunner(); + } + + return $this->runner; + } + + private function initializeRunner(): Runner + { + if ($this->container->get(InputInterface::class)->getOption('dry-run')) { + return $this->container->get(DryRunner::class); + } + + return $this->container->get(PassthruRunner::class); + } + + private function guardServiceAvailability(ContainerInterface $container): void + { + $requiredServices = [ + InputInterface::class, + PassthruRunner::class, + DryRunner::class, + ]; + + foreach ($requiredServices as $serviceId) { + if (!$container->has($serviceId)) { + throw new class(\sprintf('The service "%s" is missing.', $serviceId)) extends \LogicException implements ContainerExceptionInterface { + }; + } + } + } +} diff --git a/src/Cli/ServiceContainer.php b/src/Cli/ServiceContainer.php index c0ec5da5..fc452733 100644 --- a/src/Cli/ServiceContainer.php +++ b/src/Cli/ServiceContainer.php @@ -7,9 +7,12 @@ use Psr\Container\NotFoundExceptionInterface; use RuntimeException; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; use Zalas\Toolbox\Cli\Command\InstallCommand; use Zalas\Toolbox\Cli\Command\ListCommand; use Zalas\Toolbox\Cli\Command\TestCommand; +use Zalas\Toolbox\Cli\Runner\DryRunner; +use Zalas\Toolbox\Cli\Runner\LazyRunner; use Zalas\Toolbox\Json\JsonTools; use Zalas\Toolbox\Runner\PassthruRunner; use Zalas\Toolbox\Runner\Runner; @@ -25,6 +28,8 @@ class ServiceContainer implements ContainerInterface ListCommand::class => 'createListCommand', TestCommand::class => 'createTestCommand', Runner::class => 'createRunner', + DryRunner::class => 'createDryRunner', + PassthruRunner::class => 'createPassthruRunner', InstallTools::class => 'createInstallToolsUseCase', ListTools::class => 'createListToolsUseCase', TestTools::class => 'createTestToolsUseCase', @@ -33,6 +38,7 @@ class ServiceContainer implements ContainerInterface private $runtimeServices = [ InputInterface::class => null, + OutputInterface::class => null, ]; public function set(string $id, /*object */$service): void @@ -86,10 +92,20 @@ private function createTestCommand(): TestCommand } private function createRunner(): Runner + { + return new LazyRunner($this); + } + + private function createPassthruRunner(): Runner { return new PassthruRunner(); } + private function createDryRunner(): Runner + { + return new DryRunner($this->get(OutputInterface::class)); + } + private function createInstallToolsUseCase(): InstallTools { return new InstallTools($this->get(Tools::class)); diff --git a/tests/Cli/ApplicationTest.php b/tests/Cli/ApplicationTest.php index 5b1ce8f3..ccff7cca 100644 --- a/tests/Cli/ApplicationTest.php +++ b/tests/Cli/ApplicationTest.php @@ -3,11 +3,14 @@ namespace Zalas\Toolbox\Tests\Cli; use PHPUnit\Framework\TestCase; +use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use Symfony\Component\Console\Application as CliApplication; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\NullOutput; +use Symfony\Component\Console\Output\OutputInterface; use Zalas\Toolbox\Cli\Application; +use Zalas\Toolbox\Cli\Command\InstallCommand; use Zalas\Toolbox\Cli\Command\ListCommand; use Zalas\Toolbox\Cli\ServiceContainer; @@ -60,9 +63,7 @@ public function test_it_takes_the_tools_option_default_from_environment_if_prese */ public function test_it_allows_to_override_tools_location() { - $container = new ServiceContainer(); - - $app = new Application(self::VERSION, $container); + $app = new Application(self::VERSION, new ServiceContainer()); $result = $app->doRun( new ArrayInput([ 'command' => ListCommand::NAME, @@ -74,4 +75,28 @@ public function test_it_allows_to_override_tools_location() $this->assertSame(0, $result); } + + /** + * @group integration + */ + public function test_it_runs_the_command_in_dry_run_mode() + { + $output = $this->prophesize(OutputInterface::class); + + $app = new Application(self::VERSION, new ServiceContainer()); + $app->doRun( + new ArrayInput([ + 'command' => InstallCommand::NAME, + '--dry-run' => true, + '--tools' => [__DIR__.'/../resources/tools.json'], + '--no-interaction' => true, + ]), + $output->reveal() + ); + + $output->writeln(Argument::allOf( + Argument::type('string'), + Argument::containingString('composer global bin phpstan require') + ))->shouldHaveBeenCalled(); + } } diff --git a/tests/Cli/Command/InstallCommandTest.php b/tests/Cli/Command/InstallCommandTest.php index fde32155..04e2d2f2 100644 --- a/tests/Cli/Command/InstallCommandTest.php +++ b/tests/Cli/Command/InstallCommandTest.php @@ -54,6 +54,11 @@ public function test_it_returns_the_status_code_of_the_run() $this->assertSame(1, $tester->getStatusCode()); } + public function test_it_defines_dry_run_option() + { + $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('dry-run')); + } + protected function getContainerTestDoubles(): array { return [ diff --git a/tests/Cli/Command/TestCommandTest.php b/tests/Cli/Command/TestCommandTest.php index a1454365..e258dc85 100644 --- a/tests/Cli/Command/TestCommandTest.php +++ b/tests/Cli/Command/TestCommandTest.php @@ -54,6 +54,11 @@ public function test_it_returns_the_status_code_of_the_run() $this->assertSame(1, $tester->getStatusCode()); } + public function test_it_defines_dry_run_option() + { + $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('dry-run')); + } + protected function getContainerTestDoubles(): array { return [ diff --git a/tests/Cli/Command/ToolboxCommandTestCase.php b/tests/Cli/Command/ToolboxCommandTestCase.php index 5e00d4fd..b02c3211 100644 --- a/tests/Cli/Command/ToolboxCommandTestCase.php +++ b/tests/Cli/Command/ToolboxCommandTestCase.php @@ -24,7 +24,7 @@ protected function setUp() public function test_it_provides_help() { - $this->assertNotEmpty($this->findCliCommand()->getDescription()); + $this->assertNotEmpty($this->cliCommand()->getDescription()); } protected function getContainerTestDoubles(): array @@ -34,13 +34,13 @@ protected function getContainerTestDoubles(): array protected function executeCliCommand(array $input = []): CommandTester { - $tester = new CommandTester($this->findCliCommand()); + $tester = new CommandTester($this->cliCommand()); $tester->execute($input); return $tester; } - private function findCliCommand(): Command + protected function cliCommand(): Command { return $this->app->find(static::CLI_COMMAND_NAME); } diff --git a/tests/Cli/Runner/DryRunnerTest.php b/tests/Cli/Runner/DryRunnerTest.php new file mode 100644 index 00000000..4c1cfb4c --- /dev/null +++ b/tests/Cli/Runner/DryRunnerTest.php @@ -0,0 +1,48 @@ +out = $this->prophesize(OutputInterface::class); + $this->runner = new DryRunner($this->out->reveal()); + } + + public function test_it_is_a_runner() + { + $this->assertInstanceOf(Runner::class, $this->runner); + } + + public function test_it_sends_the_command_to_the_output() + { + $result = $this->runner->run(new class implements Command { + public function __toString(): string + { + return 'echo "Foo"'; + } + }); + + $this->out->writeln('echo "Foo"')->shouldHaveBeenCalled(); + + $this->assertSame(0, $result); + } +} diff --git a/tests/Cli/Runner/LazyRunnerTest.php b/tests/Cli/Runner/LazyRunnerTest.php new file mode 100644 index 00000000..0ab6130c --- /dev/null +++ b/tests/Cli/Runner/LazyRunnerTest.php @@ -0,0 +1,137 @@ +container = $this->prophesize(ContainerInterface::class); + $this->input = $this->prophesize(InputInterface::class); + + $this->container->get(InputInterface::class)->willReturn($this->input); + $this->container->has(InputInterface::class)->willReturn(true); + $this->container->has(PassthruRunner::class)->willReturn(true); + $this->container->has(DryRunner::class)->willReturn(true); + + $this->runner = new LazyRunner($this->container->reveal()); + } + + public function test_it_is_a_runner() + { + $this->assertInstanceOf(Runner::class, $this->runner); + } + + public function test_it_runs_dry_runner_if_dry_run_option_is_present() + { + $command = $this->command(); + + $runner = $this->prophesize(Runner::class); + $runner->run($command)->willReturn(0); + + $this->input->getOption('dry-run')->willReturn(true); + $this->container->get(DryRunner::class)->willReturn($runner); + + $this->runner->run($command); + + $runner->run($command)->shouldHaveBeenCalled(); + } + + public function test_it_returns_status_code_of_the_real_runner() + { + $command = $this->command(); + + $runner = $this->prophesize(Runner::class); + $runner->run($command)->willReturn(1); + + $this->input->getOption('dry-run')->willReturn(true); + $this->container->get(DryRunner::class)->willReturn($runner); + + $this->assertSame(1, $this->runner->run($command)); + } + + public function test_it_runs_passthru_runner_if_dry_run_option_is_absent() + { + $command = $this->command(); + + $runner = $this->prophesize(Runner::class); + $runner->run($command)->willReturn(0); + + $this->input->getOption('dry-run')->willReturn(false); + $this->container->get(PassthruRunner::class)->willReturn($runner); + + $this->runner->run($command); + + $runner->run($command)->shouldHaveBeenCalled(); + } + + public function test_it_only_initializes_the_runner_once() + { + $command = $this->command(); + + $runner = $this->prophesize(Runner::class); + $runner->run($command)->willReturn(0); + + $this->input->getOption('dry-run')->willReturn(false); + $this->container->get(PassthruRunner::class)->willReturn($runner); + + $this->runner->run($command); + $this->runner->run($command); + + $this->container->get(PassthruRunner::class)->shouldHaveBeenCalledTimes(1); + } + + /** + * @dataProvider provideRequiredServices + */ + public function test_it_complains_if_any_of_services_it_needs_from_the_container_are_not_defined(string $serviceId) + { + $this->expectException(ContainerExceptionInterface::class); + $this->expectExceptionMessage(\sprintf('The service "%s" is missing.', $serviceId)); + + $this->container->has($serviceId)->willReturn(false); + $this->container->get($serviceId)->willThrow(new class extends \RuntimeException implements NotFoundExceptionInterface { + }); + + new LazyRunner($this->container->reveal()); + } + + public function provideRequiredServices() + { + yield [InputInterface::class]; + yield [PassthruRunner::class]; + yield [DryRunner::class]; + } + + private function command(): Command + { + return $this->prophesize(Command::class)->reveal(); + } +} diff --git a/tests/Cli/ServiceContainerTest.php b/tests/Cli/ServiceContainerTest.php index 2206d79c..f5028505 100644 --- a/tests/Cli/ServiceContainerTest.php +++ b/tests/Cli/ServiceContainerTest.php @@ -7,10 +7,15 @@ use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; use Zalas\Toolbox\Cli\Command\InstallCommand; use Zalas\Toolbox\Cli\Command\ListCommand; use Zalas\Toolbox\Cli\Command\TestCommand; +use Zalas\Toolbox\Cli\Runner\DryRunner; +use Zalas\Toolbox\Cli\Runner\LazyRunner; use Zalas\Toolbox\Cli\ServiceContainer; +use Zalas\Toolbox\Runner\PassthruRunner; +use Zalas\Toolbox\Runner\Runner; class ServiceContainerTest extends TestCase { @@ -22,6 +27,8 @@ class ServiceContainerTest extends TestCase protected function setUp() { $this->container = new ServiceContainer(); + $this->container->set(InputInterface::class, $this->prophesize(InputInterface::class)->reveal()); + $this->container->set(OutputInterface::class, $this->prophesize(OutputInterface::class)->reveal()); } public function test_it_is_a_psr_container() @@ -34,30 +41,31 @@ public function test_it_returns_false_if_service_is_not_registered() $this->assertFalse($this->container->has('foo')); } - public function test_it_throws_an_exception_if_unregistered_service_is_accessed() + /** + * @dataProvider provideApplicationServices + */ + public function test_it_creates_application_services(string $serviceId, string $expectedType) { - $this->expectException(NotFoundExceptionInterface::class); - $this->expectExceptionMessage('The "foo" service is not registered in the service container.'); - - $this->container->get('foo'); + $this->assertTrue($this->container->has($serviceId)); + $this->assertInstanceOf($expectedType, $this->container->get($serviceId)); } - public function test_it_creates_the_install_command() + public function provideApplicationServices() { - $this->assertTrue($this->container->has(InstallCommand::class)); - $this->assertInstanceOf(InstallCommand::class, $this->container->get(InstallCommand::class)); + yield [InstallCommand::class, InstallCommand::class]; + yield [ListCommand::class, ListCommand::class]; + yield [TestCommand::class, TestCommand::class]; + yield [Runner::class, LazyRunner::class]; + yield [DryRunner::class, DryRunner::class]; + yield [PassthruRunner::class, PassthruRunner::class]; } - public function test_it_creates_the_list_command() + public function test_it_throws_an_exception_if_unregistered_service_is_accessed() { - $this->assertTrue($this->container->has(ListCommand::class)); - $this->assertInstanceOf(ListCommand::class, $this->container->get(ListCommand::class)); - } + $this->expectException(NotFoundExceptionInterface::class); + $this->expectExceptionMessage('The "foo" service is not registered in the service container.'); - public function test_it_creates_the_test_command() - { - $this->assertTrue($this->container->has(TestCommand::class)); - $this->assertInstanceOf(TestCommand::class, $this->container->get(TestCommand::class)); + $this->container->get('foo'); } public function test_it_registers_a_runtime_service() @@ -72,6 +80,8 @@ public function test_it_registers_a_runtime_service() public function test_it_returns_false_if_runtime_service_has_not_been_defined() { + $this->container = new ServiceContainer(); + $this->assertFalse($this->container->has(InputInterface::class)); } @@ -79,6 +89,7 @@ public function test_it_throws_an_exception_if_missing_runtime_service_is_access { $this->expectException(NotFoundExceptionInterface::class); + $this->container = new ServiceContainer(); $this->container->get(InputInterface::class); }