diff --git a/composer.json b/composer.json index 675ca726dafc5f9ac4f57b9d8ac122d5b1e03644..64f4d82c9d865f6e72914ece47865acfa5d54131 100644 --- a/composer.json +++ b/composer.json @@ -143,7 +143,6 @@ "magento/language-pt_br": "self.version", "magento/language-zh_cn": "self.version", "magento/framework": "self.version", - "magento/project-setup": "0.1.0", "oyejorge/less.php": "1.7.0", "trentrichardson/jquery-timepicker-addon": "1.4.3", "components/handlebars.js": "1.3.0", diff --git a/composer.lock b/composer.lock index d9dde5fab26f383959bee5f375672ca6dc80df7d..7b97eaf296a318fe02d104c15f032bca9537ba42 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "26d01745182292ebe1704daed40c6d4a", + "hash": "b2434120b753fdef32c2584eade97c9d", "packages": [ { "name": "composer/composer", @@ -1821,16 +1821,16 @@ }, { "name": "fabpot/php-cs-fixer", - "version": "v1.3", + "version": "v1.4", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "653cefbf33241185b58f7323157f1829552e370d" + "reference": "72a8c34210c0fbd8caa007fccea87a59f6f0a4d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/653cefbf33241185b58f7323157f1829552e370d", - "reference": "653cefbf33241185b58f7323157f1829552e370d", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/72a8c34210c0fbd8caa007fccea87a59f6f0a4d3", + "reference": "72a8c34210c0fbd8caa007fccea87a59f6f0a4d3", "shasum": "" }, "require": { @@ -1843,15 +1843,13 @@ "symfony/process": "~2.3", "symfony/stopwatch": "~2.5" }, + "require-dev": { + "satooshi/php-coveralls": "0.7.*@dev" + }, "bin": [ "php-cs-fixer" ], "type": "application", - "extra": { - "branch-alias": { - "dev-master": "1.3-dev" - } - }, "autoload": { "psr-4": { "Symfony\\CS\\": "Symfony/CS/" @@ -1872,20 +1870,20 @@ } ], "description": "A script to automatically fix Symfony Coding Standard", - "time": "2014-12-12 06:09:01" + "time": "2015-01-12 21:28:53" }, { "name": "league/climate", - "version": "2.6.0", + "version": "2.6.1", "source": { "type": "git", "url": "https://github.com/thephpleague/climate.git", - "reference": "776b6c3b32837832a9f6d94f134b55cb7910ece6" + "reference": "28851c909017424f61cc6a62089316313c645d1c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/climate/zipball/776b6c3b32837832a9f6d94f134b55cb7910ece6", - "reference": "776b6c3b32837832a9f6d94f134b55cb7910ece6", + "url": "https://api.github.com/repos/thephpleague/climate/zipball/28851c909017424f61cc6a62089316313c645d1c", + "reference": "28851c909017424f61cc6a62089316313c645d1c", "shasum": "" }, "require": { @@ -1921,7 +1919,7 @@ "php", "terminal" ], - "time": "2015-01-08 02:28:23" + "time": "2015-01-18 14:31:58" }, { "name": "lusitanian/oauth", @@ -2261,16 +2259,16 @@ }, { "name": "phpunit/php-token-stream", - "version": "1.3.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "f8d5d08c56de5cfd592b3340424a81733259a876" + "reference": "db32c18eba00b121c145575fcbcd4d4d24e6db74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/f8d5d08c56de5cfd592b3340424a81733259a876", - "reference": "f8d5d08c56de5cfd592b3340424a81733259a876", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/db32c18eba00b121c145575fcbcd4d4d24e6db74", + "reference": "db32c18eba00b121c145575fcbcd4d4d24e6db74", "shasum": "" }, "require": { @@ -2283,7 +2281,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3-dev" + "dev-master": "1.4-dev" } }, "autoload": { @@ -2306,7 +2304,7 @@ "keywords": [ "tokenizer" ], - "time": "2014-08-31 06:12:13" + "time": "2015-01-17 09:51:32" }, { "name": "phpunit/phpunit", diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..97039e891351231f60d6c028e57585596dfd78aa --- /dev/null +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php @@ -0,0 +1,439 @@ +<?php +/** + * Copyright © 2015 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Test\Integrity; + +use Magento\Framework\Composer\MagentoComponent; +use Magento\Framework\Test\Utility\Files; +use Magento\Framework\Shell; +use Magento\Framework\Exception; + +/** + * A test that enforces validity of composer.json files and any other conventions in Magento components + */ +class ComposerTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Magento\Framework\Shell + */ + private static $shell; + + /** + * @var bool + */ + private static $isComposerAvailable; + + /** + * @var string + */ + private static $root; + + /** + * @var \stdClass + */ + private static $rootJson; + + /** + * @var array + */ + private static $dependencies; + + /** + * @var string + */ + private static $composerPath = 'composer'; + + public static function setUpBeforeClass() + { + if (defined('TESTS_COMPOSER_PATH')) { + self::$composerPath = TESTS_COMPOSER_PATH; + } + self::$shell = self::createShell(); + self::$isComposerAvailable = self::isComposerAvailable(); + self::$root = Files::init()->getPathToSource(); + self::$rootJson = json_decode(file_get_contents(self::$root . '/composer.json'), true); + self::$dependencies = []; + } + + public function testValidComposerJson() + { + $invoker = new \Magento\Framework\Test\Utility\AggregateInvoker($this); + $invoker( + /** + * @param string $dir + * @param string $packageType + */ + function ($dir, $packageType) { + $this->assertComposerAvailable(); + $file = $dir . '/composer.json'; + $this->assertFileExists($file); + self::$shell->execute(self::$composerPath . ' validate --working-dir=%s', [$dir]); + $contents = file_get_contents($file); + $json = json_decode($contents); + $this->assertCodingStyle($contents); + $this->assertMagentoConventions($dir, $packageType, $json); + }, + $this->validateComposerJsonDataProvider() + ); + } + + /** + * @return array + */ + public function validateComposerJsonDataProvider() + { + $root = \Magento\Framework\Test\Utility\Files::init()->getPathToSource(); + $result = []; + foreach (glob("{$root}/app/code/Magento/*", GLOB_ONLYDIR) as $dir) { + $result[$dir] = [$dir, 'magento2-module']; + } + foreach (glob("{$root}/app/i18n/magento/*", GLOB_ONLYDIR) as $dir) { + $result[$dir] = [$dir, 'magento2-language']; + } + foreach (glob("{$root}/app/design/adminhtml/Magento/*", GLOB_ONLYDIR) as $dir) { + $result[$dir] = [$dir, 'magento2-theme']; + } + foreach (glob("{$root}/app/design/frontend/Magento/*", GLOB_ONLYDIR) as $dir) { + $result[$dir] = [$dir, 'magento2-theme']; + } + foreach (glob("{$root}/lib/internal/Magento/*", GLOB_ONLYDIR) as $dir) { + $result[$dir] = [$dir, 'magento2-library']; + } + $result[$root] = [$root, 'project']; + + return $result; + } + + /** + * Some of coding style conventions + * + * @param string $contents + */ + private function assertCodingStyle($contents) + { + $this->assertNotRegExp('/" :\s*["{]/', $contents, 'Coding style: no space before colon.'); + $this->assertNotRegExp('/":["{]/', $contents, 'Coding style: a space is necessary after colon.'); + } + + /** + * Enforce Magento-specific conventions to a composer.json file + * + * @param string $dir + * @param string $packageType + * @param \StdClass $json + * @throws \InvalidArgumentException + */ + private function assertMagentoConventions($dir, $packageType, \StdClass $json) + { + $this->assertObjectHasAttribute('name', $json); + $this->assertObjectHasAttribute('license', $json); + $this->assertObjectHasAttribute('type', $json); + $this->assertObjectHasAttribute('version', $json); + $this->assertVersionInSync($json->name, $json->version); + $this->assertObjectHasAttribute('require', $json); + $this->assertEquals($packageType, $json->type); + if ($packageType !== 'project') { + self::$dependencies[] = $json->name; + $this->assertHasMap($json); + $this->assertMapConsistent($dir, $json); + } + switch ($packageType) { + case 'magento2-module': + $xml = simplexml_load_file("$dir/etc/module.xml"); + $this->assertConsistentModuleName($xml, $json->name); + $this->assertDependsOnPhp($json->require); + $this->assertDependsOnFramework($json->require); + $this->assertDependsOnInstaller($json->require); + $this->assertRequireInSync($json); + break; + case 'magento2-language': + $this->assertRegExp('/^magento\/language\-[a-z]{2}_[a-z]{2}$/', $json->name); + $this->assertDependsOnFramework($json->require); + $this->assertDependsOnInstaller($json->require); + $this->assertRequireInSync($json); + break; + case 'magento2-theme': + $this->assertRegExp('/^magento\/theme-(?:adminhtml|frontend)(\-[a-z0-9_]+)+$/', $json->name); + $this->assertDependsOnPhp($json->require); + $this->assertDependsOnFramework($json->require); + $this->assertDependsOnInstaller($json->require); + $this->assertRequireInSync($json); + break; + case 'magento2-library': + $this->assertDependsOnPhp($json->require); + $this->assertRegExp('/^magento\/framework$/', $json->name); + $this->assertDependsOnInstaller($json->require); + $this->assertRequireInSync($json); + break; + case 'project': + sort(self::$dependencies); + $dependenciesListed = []; + foreach (array_keys((array)self::$rootJson['replace']) as $key) { + if (MagentoComponent::matchMagentoComponent($key)) { + $dependenciesListed[] = $key; + } + } + sort($dependenciesListed); + $nonDeclaredDependencies = array_diff(self::$dependencies, $dependenciesListed); + $nonexistentDependencies = array_diff($dependenciesListed, self::$dependencies); + $this->assertEmpty( + $nonDeclaredDependencies, + 'Following dependencies are not declared in the root composer.json: ' + . join(', ', $nonDeclaredDependencies) + ); + $this->assertEmpty( + $nonexistentDependencies, + 'Following dependencies declared in the root composer.json do not exist: ' + . join(', ', $nonexistentDependencies) + ); + break; + default: + throw new \InvalidArgumentException("Unknown package type {$packageType}"); + } + } + + /** + * Assert that there is map in specified composer json + * + * @param \StdClass $json + */ + private function assertHasMap(\StdClass $json) + { + $error = 'There must be an "extra->map" node in composer.json of each Magento component.'; + $this->assertObjectHasAttribute('extra', $json, $error); + $this->assertObjectHasAttribute('map', $json->extra, $error); + $this->assertInternalType('array', $json->extra->map, $error); + } + + /** + * Assert that component directory name and mapping information are consistent + * + * @param string $dir + * @param \StdClass $json + */ + private function assertMapConsistent($dir, $json) + { + preg_match('/^.+\/(.+)\/(.+)$/', $dir, $matches); + list(, $vendor, $name) = $matches; + $map = $json->extra->map; + $this->assertArrayHasKey(0, $map); + $this->assertArrayHasKey(1, $map[0]); + $this->assertRegExp( + "/{$vendor}\\/{$name}$/", + $map[0][1], + 'Mapping info is inconsistent with the directory structure' + ); + } + + /** + * Enforce package naming conventions for modules + * + * @param \SimpleXMLElement $xml + * @param string $packageName + */ + private function assertConsistentModuleName(\SimpleXMLElement $xml, $packageName) + { + $moduleName = (string)$xml->module->attributes()->name; + $this->assertEquals( + $packageName, + $this->convertModuleToPackageName($moduleName), + "For the module '{$moduleName}', the expected package name is '{$packageName}'" + ); + } + + /** + * Make sure a component depends on php version + * + * @param \StdClass $json + */ + private function assertDependsOnPhp(\StdClass $json) + { + $this->assertObjectHasAttribute('php', $json, 'This component is expected to depend on certain PHP version(s)'); + } + + /** + * Make sure a component depends on magento/framework component + * + * @param \StdClass $json + */ + private function assertDependsOnFramework(\StdClass $json) + { + $this->assertObjectHasAttribute( + 'magento/framework', + $json, + 'This component is expected to depend on magento/framework' + ); + } + + /** + * Make sure a component depends on Magento Composer Installer component + * + * @param \StdClass $json + */ + private function assertDependsOnInstaller(\StdClass $json) + { + $this->assertObjectHasAttribute( + 'magento/magento-composer-installer', + $json, + 'This component is expected to depend on magento/magento-composer-installer' + ); + } + + /** + * Assert that versions in root composer.json and Magento component's composer.json are not out of sync + * + * @param string $name + * @param string $version + */ + private function assertVersionInSync($name, $version) + { + $this->assertEquals( + self::$rootJson['version'], + $version, + "Version {$version} in component {$name} is inconsistent with version " + . self::$rootJson['version'] . ' in root composer.json' + ); + } + + /** + * Make sure requirements of components are reflected in root composer.json + * + * @param \StdClass $json + */ + private function assertRequireInSync(\StdClass $json) + { + $name = $json->name; + if (isset($json->require)) { + $errors = []; + foreach (array_keys((array)$json->require) as $depName) { + if ($depName == 'magento/magento-composer-installer') { + // Magento Composer Installer is not needed for already existing components + continue; + } + if (!isset(self::$rootJson['require-dev'][$depName]) && !isset(self::$rootJson['require'][$depName]) + && !isset(self::$rootJson['replace'][$depName])) { + $errors[] = "'$name' depends on '$depName'"; + } + } + if (!empty($errors)) { + $this->fail( + "The following dependencies are missing in root 'composer.json'," + . " while declared in child components.\n" + . "Consider adding them to 'require-dev' section (if needed for child components only)," + . " to 'replace' section (if they are present in the project)," + . " to 'require' section (if needed for the skeleton).\n" + . join("\n", $errors) + ); + } + } + } + + /** + * Convert a fully qualified module name to a composer package name according to conventions + * + * @param string $moduleName + * @return string + */ + private function convertModuleToPackageName($moduleName) + { + list($vendor, $name) = explode('_', $moduleName, 2); + $package = 'module'; + foreach (preg_split('/([A-Z][a-z\d]+)/', $name, -1, PREG_SPLIT_DELIM_CAPTURE) as $chunk) { + $package .= $chunk ? "-{$chunk}" : ''; + } + return strtolower("{$vendor}/{$package}"); + } + + /** + * Create shell wrapper + * + * @return \Magento\Framework\Shell + */ + private static function createShell() + { + return new Shell(new Shell\CommandRenderer, null); + } + + /** + * Check if composer command is available in the environment + * + * @return bool + */ + private static function isComposerAvailable() + { + try { + self::$shell->execute(self::$composerPath . ' --version'); + } catch (Exception $e) { + return false; + } + return true; + } + + /** + * Skip the test if composer is unavailable + */ + private function assertComposerAvailable() + { + if (!self::$isComposerAvailable) { + $this->markTestSkipped(); + } + } + + public function testComponentPathsInRoot() + { + if (!isset(self::$rootJson['extra']) || !isset(self::$rootJson['extra']['component_paths'])) { + $this->markTestSkipped("The root composer.json file doesn't mention any extra component paths information"); + } + $this->assertArrayHasKey( + 'replace', + self::$rootJson, + "If there are any component paths specified, then they must be reflected in 'replace' section" + ); + $flat = $this->getFlatPathsInfo(self::$rootJson['extra']['component_paths']); + while (list(, list($component, $path)) = each($flat)) { + $this->assertFileExists( + self::$root . '/' . $path, + "Missing or invalid component path: {$component} -> {$path}" + ); + $this->assertArrayHasKey( + $component, + self::$rootJson['replace'], + "The {$component} is specified in 'extra->component_paths', but missing in 'replace' section" + ); + } + foreach (array_keys(self::$rootJson['replace']) as $replace) { + if (!MagentoComponent::matchMagentoComponent($replace)) { + $this->assertArrayHasKey( + $replace, + self::$rootJson['extra']['component_paths'], + "The {$replace} is specified in 'replace', but missing in 'extra->component_paths' section" + ); + } + } + } + + /** + * @param array $info + * @return array + * @throws \Exception + */ + private function getFlatPathsInfo(array $info) + { + $flat = []; + foreach ($info as $key => $element) { + if (is_string($element)) { + $flat[] = [$key, $element]; + } elseif (is_array($element)) { + foreach ($element as $path) { + $flat[] = [$key, $path]; + } + } else { + throw new \Exception("Unexpected element 'in extra->component_paths' section"); + } + } + + return $flat; + } +} diff --git a/lib/internal/Magento/Framework/Composer/MagentoComponent.php b/lib/internal/Magento/Framework/Composer/MagentoComponent.php new file mode 100644 index 0000000000000000000000000000000000000000..5e2da7bfa557e166d6f45e04efd1a584dac92a30 --- /dev/null +++ b/lib/internal/Magento/Framework/Composer/MagentoComponent.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © 2015 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Framework\Composer; + +class MagentoComponent +{ + /** + * Get matched Magento component or empty array, if it's not a Magento component + * + * @param string $key + * @return string[] ['type' => '<type>', 'area' => '<area>', 'name' => '<name>'] + * Ex.: ['type' => 'module', 'name' => 'catalog'] + * ['type' => 'theme', 'area' => 'frontend', 'name' => 'blank'] + */ + public static function matchMagentoComponent($key) + { + $typePattern = 'module|theme|language|framework'; + $areaPattern = 'frontend|adminhtml'; + $namePattern = '[a-z_-]+'; + $regex = '/^magento\/(?P<type>' . $typePattern . ')(?:-(?P<area>' . $areaPattern . '))?(?:-(?P<name>' + . $namePattern . '))?$/'; + if (preg_match($regex, $key, $matches)) { + return $matches; + } + return []; + } +} diff --git a/lib/internal/Magento/Framework/Composer/README.md b/lib/internal/Magento/Framework/Composer/README.md new file mode 100644 index 0000000000000000000000000000000000000000..fc92f73c2db20310df2d28e92c12972b78c8f80d --- /dev/null +++ b/lib/internal/Magento/Framework/Composer/README.md @@ -0,0 +1,2 @@ +**Magento\Framework\Composer** provides Magento-specific features for working with packages. + For example, ability to distinguish Magento package and any other package.