From ed87bf68202aaf80a48535366c49a5b8dec6cf9f Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Wed, 17 Dec 2014 13:17:29 -0600 Subject: [PATCH 1/6] MAGETWO-31732: Blacklisted ComposerTest - restored the test for CE --- .../Magento/Test/Integrity/ComposerTest.php | 418 ++++++++++++++++++ .../Framework/Composer/MagentoComponent.php | 28 ++ 2 files changed, 446 insertions(+) create mode 100644 dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php create mode 100644 lib/internal/Magento/Framework/Composer/MagentoComponent.php 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 0000000000000..f79d5aaae0748 --- /dev/null +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php @@ -0,0 +1,418 @@ +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 = []; + foreach (self::$rootJson['extra']['component_paths'] 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"); + } + } + 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" + ); + } + } +} diff --git a/lib/internal/Magento/Framework/Composer/MagentoComponent.php b/lib/internal/Magento/Framework/Composer/MagentoComponent.php new file mode 100644 index 0000000000000..4ec96eeed9ff6 --- /dev/null +++ b/lib/internal/Magento/Framework/Composer/MagentoComponent.php @@ -0,0 +1,28 @@ + '', 'area' => '', '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' . $typePattern . ')(?:-(?P' . $areaPattern . '))?(?:-(?P' + . $namePattern . '))?$/'; + if (preg_match($regex, $key, $matches)) { + return $matches; + } + return []; + } +} From 45921f2bf8f46bd3ea3a1799d9abbcce09212940 Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Wed, 24 Dec 2014 17:31:25 -0600 Subject: [PATCH 2/6] MAGETWO-31732: Blacklisted ComposerTest - added test for forgotten "replace" packages --- composer.json | 2 -- .../testsuite/Magento/Test/Integrity/ComposerTest.php | 9 +++++++++ .../Magento/Framework/Composer/MagentoComponent.php | 3 ++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 7d2f652018410..8735194635f94 100644 --- a/composer.json +++ b/composer.json @@ -143,9 +143,7 @@ "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", - "symfony/yaml": "2.3.x-dev", "trentrichardson/jquery-timepicker-addon": "1.4.3", "components/handlebars.js": "1.3.0", "colinmollenhour/cache-backend-redis": "dev-master#193d377b7fb2e88595578b282fa01a62d1185abc", diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php index f79d5aaae0748..87ee01254e88b 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php @@ -414,5 +414,14 @@ public function testComponentPathsInRoot() "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" + ); + } + } } } diff --git a/lib/internal/Magento/Framework/Composer/MagentoComponent.php b/lib/internal/Magento/Framework/Composer/MagentoComponent.php index 4ec96eeed9ff6..b73b873e68d71 100644 --- a/lib/internal/Magento/Framework/Composer/MagentoComponent.php +++ b/lib/internal/Magento/Framework/Composer/MagentoComponent.php @@ -4,7 +4,8 @@ */ namespace Magento\Framework\Composer; -class MagentoComponent { +class MagentoComponent +{ /** * Get matched Magento component or empty array, if it's not a Magento component * From 7be10014adecd88f6d1da8418c4a2e559edf2748 Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Tue, 6 Jan 2015 12:23:36 -0600 Subject: [PATCH 3/6] MAGETWO-31732: Blacklisted ComposerTest - updated composer.lock after merge --- composer.lock | 80 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 16 deletions(-) diff --git a/composer.lock b/composer.lock index d2fc6da7fe7b8..c21f6ae9496b3 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": "d3a510fb8a6b17c084148509d02478d4", + "hash": "18b11d313f169ab82027772ba8d02278", "packages": [ { "name": "composer/composer", @@ -290,16 +290,16 @@ }, { "name": "seld/jsonlint", - "version": "1.3.0", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "a7bc2ec9520ad15382292591b617c43bdb1fec35" + "reference": "863ae85c6d3ef60ca49cb12bd051c4a0648c40c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/a7bc2ec9520ad15382292591b617c43bdb1fec35", - "reference": "a7bc2ec9520ad15382292591b617c43bdb1fec35", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/863ae85c6d3ef60ca49cb12bd051c4a0648c40c4", + "reference": "863ae85c6d3ef60ca49cb12bd051c4a0648c40c4", "shasum": "" }, "require": { @@ -332,7 +332,7 @@ "parser", "validator" ], - "time": "2014-09-05 15:36:20" + "time": "2015-01-04 21:18:15" }, { "name": "symfony/console", @@ -2001,16 +2001,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "2.0.13", + "version": "2.0.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "0e7d2eec5554f869fa7a4ec2d21e4b37af943ea5" + "reference": "ca158276c1200cc27f5409a5e338486bc0b4fc94" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/0e7d2eec5554f869fa7a4ec2d21e4b37af943ea5", - "reference": "0e7d2eec5554f869fa7a4ec2d21e4b37af943ea5", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca158276c1200cc27f5409a5e338486bc0b4fc94", + "reference": "ca158276c1200cc27f5409a5e338486bc0b4fc94", "shasum": "" }, "require": { @@ -2062,7 +2062,7 @@ "testing", "xunit" ], - "time": "2014-12-03 06:41:44" + "time": "2014-12-26 13:28:33" }, { "name": "phpunit/php-file-iterator", @@ -2608,16 +2608,16 @@ }, { "name": "sebastian/version", - "version": "1.0.3", + "version": "1.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "b6e1f0cf6b9e1ec409a0d3e2f2a5fb0998e36b43" + "reference": "a77d9123f8e809db3fbdea15038c27a95da4058b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/b6e1f0cf6b9e1ec409a0d3e2f2a5fb0998e36b43", - "reference": "b6e1f0cf6b9e1ec409a0d3e2f2a5fb0998e36b43", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/a77d9123f8e809db3fbdea15038c27a95da4058b", + "reference": "a77d9123f8e809db3fbdea15038c27a95da4058b", "shasum": "" }, "type": "library", @@ -2639,7 +2639,7 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", - "time": "2014-03-07 15:35:33" + "time": "2014-12-15 14:25:24" }, { "name": "sjparkinson/static-review", @@ -3025,6 +3025,53 @@ "description": "Symfony Stopwatch Component", "homepage": "http://symfony.com", "time": "2014-12-02 20:19:20" + }, + { + "name": "symfony/yaml", + "version": "v2.6.1", + "target-dir": "Symfony/Component/Yaml", + "source": { + "type": "git", + "url": "https://github.com/symfony/Yaml.git", + "reference": "3346fc090a3eb6b53d408db2903b241af51dcb20" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/Yaml/zipball/3346fc090a3eb6b53d408db2903b241af51dcb20", + "reference": "3346fc090a3eb6b53d408db2903b241af51dcb20", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "psr-0": { + "Symfony\\Component\\Yaml\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Symfony Yaml Component", + "homepage": "http://symfony.com", + "time": "2014-12-02 20:19:20" } ], "aliases": [], @@ -3034,6 +3081,7 @@ "phpmd/phpmd": 0 }, "prefer-stable": false, + "prefer-lowest": false, "platform": { "php": "~5.4.11|~5.5.0" }, From 19e01b8b24a5903c0daf8eee6c030c60b7d2821b Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Mon, 12 Jan 2015 17:42:46 -0600 Subject: [PATCH 4/6] MAGETWO-31732: Blacklisted ComposerTest - fixed copyright --- .../static/testsuite/Magento/Test/Integrity/ComposerTest.php | 4 ++-- lib/internal/Magento/Framework/Composer/MagentoComponent.php | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php index 87ee01254e88b..43e87f161c750 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php @@ -1,8 +1,8 @@ Date: Thu, 15 Jan 2015 14:36:03 -0600 Subject: [PATCH 5/6] MAGETWO-31732: Blacklisted ComposerTest - added readme for \Magento\Framework\Composer --- lib/internal/Magento/Framework/Composer/README.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 lib/internal/Magento/Framework/Composer/README.md diff --git a/lib/internal/Magento/Framework/Composer/README.md b/lib/internal/Magento/Framework/Composer/README.md new file mode 100644 index 0000000000000..fc92f73c2db20 --- /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. From 8312d10d357639f0a3494b633ed2477c3fc284c2 Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Fri, 16 Jan 2015 17:23:44 -0600 Subject: [PATCH 6/6] MAGETWO-31732: Blacklisted ComposerTest - reduced complexity of a test --- .../Magento/Test/Integrity/ComposerTest.php | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php index 43e87f161c750..97039e8913512 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php @@ -391,18 +391,7 @@ public function testComponentPathsInRoot() self::$rootJson, "If there are any component paths specified, then they must be reflected in 'replace' section" ); - $flat = []; - foreach (self::$rootJson['extra']['component_paths'] 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"); - } - } + $flat = $this->getFlatPathsInfo(self::$rootJson['extra']['component_paths']); while (list(, list($component, $path)) = each($flat)) { $this->assertFileExists( self::$root . '/' . $path, @@ -424,4 +413,27 @@ public function testComponentPathsInRoot() } } } + + /** + * @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; + } }