diff --git a/composer.json b/composer.json index 961a80d5..8068f449 100644 --- a/composer.json +++ b/composer.json @@ -20,12 +20,15 @@ "sort-packages": true }, "require": { - "php": "^7.0" + "php": "^7.0", + "localheinz/json-normalizer": "dev-master#3a07f98" }, "require-dev": { + "composer/composer": "^1.0.0", "infection/infection": "~0.7.0", "localheinz/php-cs-fixer-config": "~1.11.0", "localheinz/test-util": "0.6.1", + "mikey179/vfsStream": "^1.6.5", "phpunit/phpunit": "^6.5.5" }, "autoload": { diff --git a/composer.lock b/composer.lock index 302af313..4a1cdd4c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,9 +4,240 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "2af070ef991d194baf2fb3c89b9eb741", - "packages": [], + "content-hash": "8752291b7dd4a338044185fc8c745181", + "packages": [ + { + "name": "localheinz/json-normalizer", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/localheinz/json-normalizer.git", + "reference": "3a07f98" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/localheinz/json-normalizer/zipball/3a07f98", + "reference": "3a07f98", + "shasum": "" + }, + "require": { + "localheinz/json-printer": "^1.0.0", + "php": "^7.0" + }, + "require-dev": { + "infection/infection": "~0.7.0", + "localheinz/php-cs-fixer-config": "~1.11.0", + "localheinz/test-util": "0.6.1", + "phpbench/phpbench": "~0.14.0", + "phpspec/prophecy": "^1.7.1", + "phpunit/phpunit": "^6.5.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Localheinz\\Json\\Normalizer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com" + } + ], + "description": "Provides normalizers for normalizing JSON documents.", + "keywords": [ + "json", + "normalizer" + ], + "time": "2018-01-12T16:48:02+00:00" + }, + { + "name": "localheinz/json-printer", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/localheinz/json-printer.git", + "reference": "c5aba96ad796560651770bcd16be8b19f324c343" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/localheinz/json-printer/zipball/c5aba96ad796560651770bcd16be8b19f324c343", + "reference": "c5aba96ad796560651770bcd16be8b19f324c343", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "infection/infection": "~0.7.0", + "localheinz/php-cs-fixer-config": "~1.9.0", + "localheinz/test-util": "0.6.1", + "phpbench/phpbench": "~0.14.0", + "phpunit/phpunit": "^6.5.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Localheinz\\Json\\Printer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com" + } + ], + "description": "Provides a JSON printer, allowing for flexible indentation.", + "keywords": [ + "formatter", + "json", + "printer" + ], + "time": "2018-01-05T18:08:01+00:00" + } + ], "packages-dev": [ + { + "name": "composer/ca-bundle", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/composer/ca-bundle.git", + "reference": "943b2c4fcad1ef178d16a713c2468bf7e579c288" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/943b2c4fcad1ef178d16a713c2468bf7e579c288", + "reference": "943b2c4fcad1ef178d16a713c2468bf7e579c288", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^5.3.2 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35", + "psr/log": "^1.0", + "symfony/process": "^2.5 || ^3.0 || ^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\CaBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], + "time": "2017-11-29T09:37:33+00:00" + }, + { + "name": "composer/composer", + "version": "1.6.2", + "source": { + "type": "git", + "url": "https://github.com/composer/composer.git", + "reference": "db191abd24b0be110c98ba2271ca992e1c70962f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/composer/zipball/db191abd24b0be110c98ba2271ca992e1c70962f", + "reference": "db191abd24b0be110c98ba2271ca992e1c70962f", + "shasum": "" + }, + "require": { + "composer/ca-bundle": "^1.0", + "composer/semver": "^1.0", + "composer/spdx-licenses": "^1.2", + "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0", + "php": "^5.3.2 || ^7.0", + "psr/log": "^1.0", + "seld/cli-prompt": "^1.0", + "seld/jsonlint": "^1.4", + "seld/phar-utils": "^1.0", + "symfony/console": "^2.7 || ^3.0 || ^4.0", + "symfony/filesystem": "^2.7 || ^3.0 || ^4.0", + "symfony/finder": "^2.7 || ^3.0 || ^4.0", + "symfony/process": "^2.7 || ^3.0 || ^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7", + "phpunit/phpunit-mock-objects": "^2.3 || ^3.0" + }, + "suggest": { + "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages", + "ext-zip": "Enabling the zip extension allows you to unzip archives", + "ext-zlib": "Allow gzip compression of HTTP requests" + }, + "bin": [ + "bin/composer" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\": "src/Composer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Composer helps you declare, manage and install dependencies of PHP projects, ensuring you have the right stack everywhere.", + "homepage": "https://getcomposer.org/", + "keywords": [ + "autoload", + "dependency", + "package" + ], + "time": "2018-01-05T14:28:42+00:00" + }, { "name": "composer/semver", "version": "1.4.2", @@ -69,6 +300,67 @@ ], "time": "2016-08-30T16:08:34+00:00" }, + { + "name": "composer/spdx-licenses", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/composer/spdx-licenses.git", + "reference": "2d899e9b33023c631854f36c39ef9f8317a7ab33" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/2d899e9b33023c631854f36c39ef9f8317a7ab33", + "reference": "2d899e9b33023c631854f36c39ef9f8317a7ab33", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5", + "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Spdx\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "SPDX licenses list and validation library.", + "keywords": [ + "license", + "spdx", + "validator" + ], + "time": "2018-01-03T16:37:06+00:00" + }, { "name": "doctrine/annotations", "version": "v1.4.0", @@ -493,6 +785,72 @@ ], "time": "2017-12-22T23:03:31+00:00" }, + { + "name": "justinrainbow/json-schema", + "version": "5.2.6", + "source": { + "type": "git", + "url": "https://github.com/justinrainbow/json-schema.git", + "reference": "d283e11b6e14c6f4664cf080415c4341293e5bbd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/d283e11b6e14c6f4664cf080415c4341293e5bbd", + "reference": "d283e11b6e14c6f4664cf080415c4341293e5bbd", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.1", + "json-schema/json-schema-test-suite": "1.2.0", + "phpunit/phpunit": "^4.8.22" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "time": "2017-10-21T13:15:38+00:00" + }, { "name": "localheinz/classy", "version": "0.3.0", @@ -624,6 +982,52 @@ ], "time": "2018-01-01T18:11:24+00:00" }, + { + "name": "mikey179/vfsStream", + "version": "v1.6.5", + "source": { + "type": "git", + "url": "https://github.com/mikey179/vfsStream.git", + "reference": "d5fec95f541d4d71c4823bb5e30cf9b9e5b96145" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mikey179/vfsStream/zipball/d5fec95f541d4d71c4823bb5e30cf9b9e5b96145", + "reference": "d5fec95f541d4d71c4823bb5e30cf9b9e5b96145", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "org\\bovigo\\vfs\\": "src/main/php" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Frank Kleine", + "homepage": "http://frankkleine.de/", + "role": "Developer" + } + ], + "description": "Virtual file system to mock the real file system in unit tests.", + "homepage": "http://vfs.bovigo.org/", + "time": "2017-08-01T08:02:14+00:00" + }, { "name": "myclabs/deep-copy", "version": "1.7.0", @@ -2339,6 +2743,147 @@ "homepage": "https://github.com/sebastianbergmann/version", "time": "2016-10-03T07:35:21+00:00" }, + { + "name": "seld/cli-prompt", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/cli-prompt.git", + "reference": "a19a7376a4689d4d94cab66ab4f3c816019ba8dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/cli-prompt/zipball/a19a7376a4689d4d94cab66ab4f3c816019ba8dd", + "reference": "a19a7376a4689d4d94cab66ab4f3c816019ba8dd", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Seld\\CliPrompt\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" + } + ], + "description": "Allows you to prompt for user input on the command line, and optionally hide the characters they type", + "keywords": [ + "cli", + "console", + "hidden", + "input", + "prompt" + ], + "time": "2017-03-18T11:32:45+00:00" + }, + { + "name": "seld/jsonlint", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "9b355654ea99460397b89c132b5c1087b6bf4473" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9b355654ea99460397b89c132b5c1087b6bf4473", + "reference": "9b355654ea99460397b89c132b5c1087b6bf4473", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "bin": [ + "bin/jsonlint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "JSON Linter", + "keywords": [ + "json", + "linter", + "parser", + "validator" + ], + "time": "2018-01-03T12:13:57+00:00" + }, + { + "name": "seld/phar-utils", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/phar-utils.git", + "reference": "7009b5139491975ef6486545a39f3e6dad5ac30a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/7009b5139491975ef6486545a39f3e6dad5ac30a", + "reference": "7009b5139491975ef6486545a39f3e6dad5ac30a", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Seld\\PharUtils\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" + } + ], + "description": "PHAR file format utilities, for when PHP phars you up", + "keywords": [ + "phra" + ], + "time": "2015-10-13T18:44:15+00:00" + }, { "name": "symfony/console", "version": "v3.4.3", @@ -3101,7 +3646,9 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "localheinz/json-normalizer": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { 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; + } +}