From 1c6f7a2893f1a1f006918ab79bd9ae8702d04c40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=B6ller?= Date: Sat, 13 Jan 2018 16:33:51 +0100 Subject: [PATCH] Enhancement: Implement NormalizeCommand --- src/.gitkeep | 0 src/Command/NormalizeCommand.php | 138 ++++ test/Unit/Command/NormalizeCommandTest.php | 827 +++++++++++++++++++++ 3 files changed, 965 insertions(+) delete mode 100644 src/.gitkeep create mode 100644 src/Command/NormalizeCommand.php create mode 100644 test/Unit/Command/NormalizeCommandTest.php diff --git a/src/.gitkeep b/src/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/Command/NormalizeCommand.php b/src/Command/NormalizeCommand.php new file mode 100644 index 00000000..d0bc55c8 --- /dev/null +++ b/src/Command/NormalizeCommand.php @@ -0,0 +1,138 @@ +normalizer = $normalizer; + } + + protected function configure() + { + $this->setDescription('Normalizes composer.json according to its JSON schema (https://getcomposer.org/schema.json).'); + } + + protected function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int + { + $file = Factory::getComposerFile(); + + $io = $this->getIO(); + + if (!\file_exists($file)) { + $io->writeError(\sprintf( + '%s not found.', + $file + )); + + return 1; + } + + if (!\is_readable($file)) { + $io->writeError(\sprintf( + '%s is not readable.', + $file + )); + + return 1; + } + + if (!\is_writable($file)) { + $io->writeError(\sprintf( + '%s is not writable.', + $file + )); + + return 1; + } + + $composer = $this->getComposer(); + + $locker = $composer->getLocker(); + + if ($locker->isLocked() && !$locker->isFresh()) { + $io->writeError('The lock file is not up to date with the latest changes in composer.json, it is recommended that you run `composer update`.'); + + return 1; + } + + $json = \file_get_contents($file); + + try { + $normalized = $this->normalizer->normalize($json); + } catch (\InvalidArgumentException $exception) { + $io->writeError(\sprintf( + '%s', + $exception->getMessage() + )); + + return 1; + } catch (\RuntimeException $exception) { + $io->writeError(\sprintf( + '%s', + $exception->getMessage() + )); + + return 1; + } + + if ($json === $normalized) { + $io->write(\sprintf( + '%s is already normalized.', + $file + )); + + return 0; + } + + \file_put_contents($file, $normalized); + + if ($locker->isLocked() && 0 !== $this->updateLocker()) { + $io->writeError(\sprintf( + 'Successfully normalized %s, but could not update lock file.', + $file + )); + + return 1; + } + + $io->write(\sprintf( + 'Successfully normalized %s.', + $file + )); + + return 0; + } + + private function updateLocker(): int + { + return $this->getApplication()->run( + new Console\Input\StringInput('update --lock'), + new Console\Output\NullOutput() + ); + } +} diff --git a/test/Unit/Command/NormalizeCommandTest.php b/test/Unit/Command/NormalizeCommandTest.php new file mode 100644 index 00000000..b27d9031 --- /dev/null +++ b/test/Unit/Command/NormalizeCommandTest.php @@ -0,0 +1,827 @@ +root = vfs\vfsStream::setup('project'); + } + + protected function tearDown() + { + $this->clearComposerFile(); + } + + public function testExtendsBaseCommand() + { + $this->assertClassExtends(Command\BaseCommand::class, NormalizeCommand::class); + } + + public function testHasNameAndDescription() + { + $command = new NormalizeCommand($this->prophesize(Normalizer\NormalizerInterface::class)->reveal()); + + $this->assertSame('normalize', $command->getName()); + $this->assertSame('Normalizes composer.json according to its JSON schema (https://getcomposer.org/schema.json).', $command->getDescription()); + } + + public function testHasNoArguments() + { + $command = new NormalizeCommand($this->prophesize(Normalizer\NormalizerInterface::class)->reveal()); + + $definition = $command->getDefinition(); + + $this->assertCount(0, $definition->getArguments()); + } + + public function testHasNoOptions() + { + $command = new NormalizeCommand($this->prophesize(Normalizer\NormalizerInterface::class)->reveal()); + + $definition = $command->getDefinition(); + + $this->assertCount(0, $definition->getOptions()); + } + + public function testExecuteFailsIfComposerFileDoesNotExist() + { + $composerFile = $this->pathToNonExistentComposerFile(); + + $io = $this->prophesize(IO\ConsoleIO::class); + + $io + ->writeError(Argument::is(\sprintf( + '%s not found.', + $composerFile + ))) + ->shouldBeCalled(); + + $command = new NormalizeCommand($this->prophesize(Normalizer\NormalizerInterface::class)->reveal()); + + $command->setIO($io->reveal()); + + $tester = new Console\Tester\CommandTester($command); + + $tester->execute([]); + + $this->assertSame(1, $tester->getStatusCode()); + $this->assertFileNotExists($composerFile); + } + + public function testExecuteFailsIfComposerFileIsNotReadable() + { + $original = $this->composerFileContent(); + + $composerFile = $this->pathToComposerFileWithContent($original); + + \chmod($composerFile, 0222); + + $io = $this->prophesize(IO\ConsoleIO::class); + + $io + ->writeError(Argument::is(\sprintf( + '%s is not readable.', + $composerFile + ))) + ->shouldBeCalled(); + + $command = new NormalizeCommand($this->prophesize(Normalizer\NormalizerInterface::class)->reveal()); + + $command->setIO($io->reveal()); + + $tester = new Console\Tester\CommandTester($command); + + $tester->execute([]); + + \chmod($composerFile, 0666); + + $this->assertSame(1, $tester->getStatusCode()); + $this->assertFileExists($composerFile); + $this->assertStringEqualsFile($composerFile, $original); + } + + public function testExecuteFailsIfComposerFileIsNotWritable() + { + $original = $this->composerFileContent(); + + $composerFile = $this->pathToComposerFileWithContent($original); + + \chmod($composerFile, 0444); + + $io = $this->prophesize(IO\ConsoleIO::class); + + $io + ->writeError(Argument::is(\sprintf( + '%s is not writable.', + $composerFile + ))) + ->shouldBeCalled(); + + $command = new NormalizeCommand($this->prophesize(Normalizer\NormalizerInterface::class)->reveal()); + + $command->setIO($io->reveal()); + + $tester = new Console\Tester\CommandTester($command); + + $tester->execute([]); + + \chmod($composerFile, 0666); + + $this->assertSame(1, $tester->getStatusCode()); + $this->assertFileExists($composerFile); + $this->assertStringEqualsFile($composerFile, $original); + } + + public function testExecuteFailsIfComposerLockFileExistsAndIsNotFresh() + { + $original = $this->composerFileContent(); + + $composerFile = $this->pathToComposerFileWithContent($original); + + $io = $this->prophesize(IO\ConsoleIO::class); + + $io + ->writeError(Argument::is('The lock file is not up to date with the latest changes in composer.json, it is recommended that you run `composer update`.')) + ->shouldBeCalled(); + + $locker = $this->prophesize(Package\Locker::class); + + $locker + ->isLocked() + ->shouldBeCalled() + ->willReturn(true); + + $locker + ->isFresh() + ->shouldBeCalled() + ->willReturn(false); + + $composer = $this->prophesize(Composer::class); + + $composer + ->getLocker() + ->shouldBeCalled() + ->willReturn($locker); + + $command = new NormalizeCommand($this->prophesize(Normalizer\NormalizerInterface::class)->reveal()); + + $command->setIO($io->reveal()); + $command->setComposer($composer->reveal()); + + $tester = new Console\Tester\CommandTester($command); + + $tester->execute([]); + + $this->assertSame(1, $tester->getStatusCode()); + $this->assertFileExists($composerFile); + $this->assertStringEqualsFile($composerFile, $original); + } + + /** + * @dataProvider providerNormalizerException + * + * @param \Exception $exception + */ + public function testExecuteFailsIfNormalizerThrowsException(\Exception $exception) + { + $original = $this->composerFileContent(); + + $composerFile = $this->pathToComposerFileWithContent($original); + + $io = $this->prophesize(IO\ConsoleIO::class); + + $io + ->writeError(Argument::is(\sprintf( + '%s', + $exception->getMessage() + ))) + ->shouldBeCalled(); + + $locker = $this->prophesize(Package\Locker::class); + + $locker + ->isLocked() + ->shouldBeCalled() + ->willReturn(true); + + $locker + ->isFresh() + ->shouldBeCalled() + ->willReturn(true); + + $composer = $this->prophesize(Composer::class); + + $composer + ->getLocker() + ->shouldBeCalled() + ->willReturn($locker); + + $normalizer = $this->prophesize(Normalizer\NormalizerInterface::class); + + $normalizer + ->normalize(Argument::is($original)) + ->shouldBeCalled() + ->willThrow($exception); + + $command = new NormalizeCommand($normalizer->reveal()); + + $command->setIO($io->reveal()); + $command->setComposer($composer->reveal()); + + $tester = new Console\Tester\CommandTester($command); + + $tester->execute([]); + + $this->assertSame(1, $tester->getStatusCode()); + $this->assertFileExists($composerFile); + $this->assertStringEqualsFile($composerFile, $original); + } + + public function providerNormalizerException(): \Generator + { + $classNames = [ + \InvalidArgumentException::class, + \RuntimeException::class, + ]; + + foreach ($classNames as $className) { + yield $className => [ + new $className($this->faker()->sentence), + ]; + } + } + + public function testExecuteSucceedsIfComposerLockFileDoesNotExistAndComposerFileIsAlreadyNormalized() + { + $original = $this->composerFileContent(); + + $composerFile = $this->pathToComposerFileWithContent($original); + + $io = $this->prophesize(IO\ConsoleIO::class); + + $io + ->write(Argument::is(\sprintf( + '%s is already normalized.', + $composerFile + ))) + ->shouldBeCalled(); + + $locker = $this->prophesize(Package\Locker::class); + + $locker + ->isLocked() + ->shouldBeCalled() + ->willReturn(false); + + $composer = $this->prophesize(Composer::class); + + $composer + ->getLocker() + ->shouldBeCalled() + ->willReturn($locker); + + $normalizer = $this->prophesize(Normalizer\NormalizerInterface::class); + + $normalizer + ->normalize(Argument::is($original)) + ->shouldBeCalled() + ->willReturn($original); + + $command = new NormalizeCommand($normalizer->reveal()); + + $command->setIO($io->reveal()); + $command->setComposer($composer->reveal()); + + $tester = new Console\Tester\CommandTester($command); + + $tester->execute([]); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertFileExists($composerFile); + $this->assertStringEqualsFile($composerFile, $original); + } + + public function testExecuteSucceedsIfComposerLockFileExistsIsFreshAndComposerFileIsAlreadyNormalized() + { + $original = $this->composerFileContent(); + + $composerFile = $this->pathToComposerFileWithContent($original); + + $io = $this->prophesize(IO\ConsoleIO::class); + + $io + ->write(Argument::is(\sprintf( + '%s is already normalized.', + $composerFile + ))) + ->shouldBeCalled(); + + $locker = $this->prophesize(Package\Locker::class); + + $locker + ->isLocked() + ->shouldBeCalled() + ->willReturn(true); + + $locker + ->isFresh() + ->shouldBeCalled() + ->willReturn(true); + + $composer = $this->prophesize(Composer::class); + + $composer + ->getLocker() + ->shouldBeCalled() + ->willReturn($locker); + + $normalizer = $this->prophesize(Normalizer\NormalizerInterface::class); + + $normalizer + ->normalize(Argument::is($original)) + ->shouldBeCalled() + ->willReturn($original); + + $command = new NormalizeCommand($normalizer->reveal()); + + $command->setIO($io->reveal()); + $command->setComposer($composer->reveal()); + + $tester = new Console\Tester\CommandTester($command); + + $tester->execute([]); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertFileExists($composerFile); + $this->assertStringEqualsFile($composerFile, $original); + } + + public function testExecuteSucceedsIfComposerLockFileDoesNotExistAndComposerFileIsNotNormalized() + { + $original = $this->composerFileContent(); + + $normalized = \json_encode(\array_reverse(\json_decode( + $original, + true + ))); + + $composerFile = $this->pathToComposerFileWithContent($original); + + $io = $this->prophesize(IO\ConsoleIO::class); + + $io + ->write(Argument::is(\sprintf( + 'Successfully normalized %s.', + $composerFile + ))) + ->shouldBeCalled(); + + $locker = $this->prophesize(Package\Locker::class); + + $locker + ->isLocked() + ->shouldBeCalled() + ->willReturn(false); + + $composer = $this->prophesize(Composer::class); + + $composer + ->getLocker() + ->shouldBeCalled() + ->willReturn($locker); + + $normalizer = $this->prophesize(Normalizer\NormalizerInterface::class); + + $normalizer + ->normalize(Argument::is($original)) + ->shouldBeCalled() + ->willReturn($normalized); + + $command = new NormalizeCommand($normalizer->reveal()); + + $command->setIO($io->reveal()); + $command->setComposer($composer->reveal()); + + $tester = new Console\Tester\CommandTester($command); + + $tester->execute([]); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertFileExists($composerFile); + $this->assertStringEqualsFile($composerFile, $normalized); + } + + public function testExecuteSucceedsIfComposerLockFileExistsIsFreshAndComposerFileIsNotNormalized() + { + $original = $this->composerFileContent(); + + $normalized = \json_encode(\array_reverse(\json_decode( + $original, + true + ))); + + $composerFile = $this->pathToComposerFileWithContent($original); + + $io = $this->prophesize(IO\ConsoleIO::class); + + $io + ->write(Argument::is(\sprintf( + 'Successfully normalized %s.', + $composerFile + ))) + ->shouldBeCalled(); + + $locker = $this->prophesize(Package\Locker::class); + + $locker + ->isLocked() + ->shouldBeCalled() + ->willReturn(true); + + $locker + ->isFresh() + ->shouldBeCalled() + ->willReturn(true); + + $composer = $this->prophesize(Composer::class); + + $composer + ->getLocker() + ->shouldBeCalled() + ->willReturn($locker); + + $application = $this->prophesize(Application::class); + + $application + ->getHelperSet() + ->shouldBeCalled() + ->willReturn(new Console\Helper\HelperSet()); + + $application + ->getDefinition() + ->shouldBeCalled() + ->willReturn($this->createDefinitionProphecy()->reveal()); + + $application + ->run( + Argument::allOf( + Argument::type(Console\Input\StringInput::class), + Argument::that(function (Console\Input\StringInput $input) { + return 'update --lock' === (string) $input; + }) + ), + Argument::type(Console\Output\NullOutput::class) + ) + ->shouldBeCalled() + ->willReturn(0); + + $normalizer = $this->prophesize(Normalizer\NormalizerInterface::class); + + $normalizer + ->normalize(Argument::is($original)) + ->shouldBeCalled() + ->willReturn($normalized); + + $command = new NormalizeCommand($normalizer->reveal()); + + $command->setIO($io->reveal()); + $command->setComposer($composer->reveal()); + $command->setApplication($application->reveal()); + + $tester = new Console\Tester\CommandTester($command); + + $tester->execute([]); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertFileExists($composerFile); + $this->assertStringEqualsFile($composerFile, $normalized); + } + + public function testExecuteFailsIfComposerLockFileExistsIsFreshComposerFileIsNotNormalizedAndLockerCouldNotBeUpdated() + { + $original = $this->composerFileContent(); + + $normalized = \json_encode(\array_reverse(\json_decode( + $original, + true + ))); + + $composerFile = $this->pathToComposerFileWithContent($original); + + $io = $this->prophesize(IO\ConsoleIO::class); + + $io + ->writeError(Argument::is(\sprintf( + 'Successfully normalized %s, but could not update lock file.', + $composerFile + ))) + ->shouldBeCalled(); + + $locker = $this->prophesize(Package\Locker::class); + + $locker + ->isLocked() + ->shouldBeCalled() + ->willReturn(true); + + $locker + ->isFresh() + ->shouldBeCalled() + ->willReturn(true); + + $composer = $this->prophesize(Composer::class); + + $composer + ->getLocker() + ->shouldBeCalled() + ->willReturn($locker); + + $application = $this->prophesize(Application::class); + + $application + ->getHelperSet() + ->shouldBeCalled() + ->willReturn(new Console\Helper\HelperSet()); + + $application + ->getDefinition() + ->shouldBeCalled() + ->willReturn($this->createDefinitionProphecy()->reveal()); + + $application + ->run( + Argument::allOf( + Argument::type(Console\Input\StringInput::class), + Argument::that(function (Console\Input\StringInput $input) { + return 'update --lock' === (string) $input; + }) + ), + Argument::type(Console\Output\NullOutput::class) + ) + ->shouldBeCalled() + ->willReturn(1); + + $normalizer = $this->prophesize(Normalizer\NormalizerInterface::class); + + $normalizer + ->normalize(Argument::is($original)) + ->shouldBeCalled() + ->willReturn($normalized); + + $command = new NormalizeCommand($normalizer->reveal()); + + $command->setIO($io->reveal()); + $command->setComposer($composer->reveal()); + $command->setApplication($application->reveal()); + + $tester = new Console\Tester\CommandTester($command); + + $tester->execute([]); + + $this->assertSame(1, $tester->getStatusCode()); + $this->assertFileExists($composerFile); + $this->assertStringEqualsFile($composerFile, $normalized); + } + + public function testExecuteSucceedsIfComposerLockFileExistsIsFreshComposerFileIsNotNormalizedAndLockerCouldBeUpdated() + { + $original = $this->composerFileContent(); + + $normalized = \json_encode(\array_reverse(\json_decode( + $original, + true + ))); + + $composerFile = $this->pathToComposerFileWithContent($original); + + $io = $this->prophesize(IO\ConsoleIO::class); + + $io + ->write(Argument::is(\sprintf( + 'Successfully normalized %s.', + $composerFile + ))) + ->shouldBeCalled(); + + $locker = $this->prophesize(Package\Locker::class); + + $locker + ->isLocked() + ->shouldBeCalled() + ->willReturn(true); + + $locker + ->isFresh() + ->shouldBeCalled() + ->willReturn(true); + + $composer = $this->prophesize(Composer::class); + + $composer + ->getLocker() + ->shouldBeCalled() + ->willReturn($locker); + + /** + * @see \Symfony\Component\Console\Tester\CommandTester::execute() + */ + $definition = $this->prophesize(Console\Input\InputDefinition::class); + + $definition + ->hasArgument('command') + ->shouldBeCalled() + ->willReturn(false); + + $definition + ->getArguments() + ->shouldBeCalled() + ->willReturn([]); + + $definition + ->getOptions() + ->shouldBeCalled() + ->willReturn([]); + + $application = $this->prophesize(Application::class); + + $application + ->getHelperSet() + ->shouldBeCalled() + ->willReturn(new Console\Helper\HelperSet()); + + $application + ->getDefinition() + ->shouldBeCalled() + ->willReturn($definition); + + $application + ->run( + Argument::allOf( + Argument::type(Console\Input\StringInput::class), + Argument::that(function (Console\Input\StringInput $input) { + return 'update --lock' === (string) $input; + }) + ), + Argument::type(Console\Output\NullOutput::class) + ) + ->shouldBeCalled() + ->willReturn(0); + + $normalizer = $this->prophesize(Normalizer\NormalizerInterface::class); + + $normalizer + ->normalize(Argument::is($original)) + ->shouldBeCalled() + ->willReturn($normalized); + + $command = new NormalizeCommand($normalizer->reveal()); + + $command->setIO($io->reveal()); + $command->setComposer($composer->reveal()); + $command->setApplication($application->reveal()); + + $tester = new Console\Tester\CommandTester($command); + + $tester->execute([]); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertFileExists($composerFile); + $this->assertStringEqualsFile($composerFile, $normalized); + } + + private function composerFileContent(): string + { + static $content; + + if (null === $content) { + $content = \file_get_contents(__DIR__ . '/../../../composer.json'); + } + + return $content; + } + + /** + * Creates a composer.json with the specified content and returns the path to it. + * + * @param string $content + * + * @return string + */ + private function pathToComposerFileWithContent(string $content): string + { + $composerFile = $this->pathToComposerFile(); + + \file_put_contents($composerFile, $content); + + $this->useComposerFile($composerFile); + + return $composerFile; + } + + /** + * Returns the path to a non-existent composer.json. + * + * @return string + */ + private function pathToNonExistentComposerFile(): string + { + $composerFile = $this->pathToComposerFile(); + + $this->useComposerFile($composerFile); + + return $composerFile; + } + + /** + * Returns the path to a composer.json (which may not exist). + * + * @return string + */ + private function pathToComposerFile(): string + { + return $this->root->url() . '/composer.json'; + } + + /** + * @see Factory::getComposerFile() + * + * @param string $composerFile + */ + private function useComposerFile(string $composerFile) + { + \putenv(\sprintf( + 'COMPOSER=%s', + $composerFile + )); + } + + /** + * @see Factory::getComposerFile() + */ + private function clearComposerFile() + { + \putenv('COMPOSER'); + } + + /** + * @see Console\Tester\CommandTester::execute() + * + * @return Prophecy\ObjectProphecy + */ + private function createDefinitionProphecy(): Prophecy\ObjectProphecy + { + $definition = $this->prophesize(Console\Input\InputDefinition::class); + + $definition + ->hasArgument('command') + ->shouldBeCalled() + ->willReturn(false); + + $definition + ->getArguments() + ->shouldBeCalled() + ->willReturn([]); + + $definition + ->getOptions() + ->shouldBeCalled() + ->willReturn([]); + + return $definition; + } +}