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;
+ }
+}