![]() Server : Apache System : Linux server2.corals.io 4.18.0-348.2.1.el8_5.x86_64 #1 SMP Mon Nov 15 09:17:08 EST 2021 x86_64 User : corals ( 1002) PHP Version : 7.4.33 Disable Function : exec,passthru,shell_exec,system Directory : /home/corals/old/dev/tests/static/testsuite/Magento/Test/Integrity/ |
<?php /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Test\Integrity; use Magento\Framework\App\Bootstrap; use Magento\Framework\Component\ComponentRegistrar; use Magento\Framework\Composer\MagentoComponent; /** * A test that enforces validity of composer.json files and any other conventions in Magento components */ class ComposerTest extends \PHPUnit\Framework\TestCase { /** * @var string */ private static $root; /** * @var \stdClass */ private static $rootJson; /** * @var array */ private static $dependencies; /** * @var \Magento\Framework\ObjectManagerInterface */ private static $objectManager; /** * @var string[] */ private static $rootComposerModuleBlacklist = []; /** * @var string[] */ private static $moduleNameBlacklist; /** * @var string */ private static $magentoFrameworkLibraryName = 'magento/framework'; public static function setUpBeforeClass(): void { self::$root = BP; self::$rootJson = json_decode(file_get_contents(self::$root . '/composer.json'), true); self::$dependencies = []; self::$objectManager = Bootstrap::create(BP, $_SERVER)->getObjectManager(); // A block can be whitelisted and thus not be required to be public self::$rootComposerModuleBlacklist = self::getBlacklist( __DIR__ . '/_files/blacklist/composer_root_modules*.txt' ); self::$moduleNameBlacklist = self::getBlacklist(__DIR__ . '/_files/blacklist/composer_module_names*.txt'); } /** * Return aggregated blacklist * * @param string $pattern * @return string[] */ public static function getBlacklist(string $pattern) { $blacklist = []; foreach (glob($pattern) as $list) { $blacklist[] = file($list, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); } return array_merge([], ...$blacklist); } public function testValidComposerJson() { $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this); $invoker( /** * @param string $dir * @param string $packageType */ function ($dir, $packageType) { $file = $dir . '/composer.json'; $this->assertFileExists($file); $this->validateComposerJsonFile($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 = BP; $componentRegistrar = new ComponentRegistrar(); $result = []; foreach ($componentRegistrar->getPaths(ComponentRegistrar::MODULE) as $dir) { $result[$dir] = [$dir, 'magento2-module']; } foreach ($componentRegistrar->getPaths(ComponentRegistrar::LANGUAGE) as $dir) { $result[$dir] = [$dir, 'magento2-language']; } foreach ($componentRegistrar->getPaths(ComponentRegistrar::THEME) as $dir) { $result[$dir] = [$dir, 'magento2-theme']; } foreach ($componentRegistrar->getPaths(ComponentRegistrar::LIBRARY) as $dir) { $result[$dir] = [$dir, 'magento2-library']; } $result[$root] = [$root, 'project']; return $result; } /** * Validate a composer.json under the given path * * @param string $path path to composer.json */ private function validateComposerJsonFile($path) { /** @var \Magento\Framework\Composer\MagentoComposerApplicationFactory $appFactory */ $appFactory = self::$objectManager->get(\Magento\Framework\Composer\MagentoComposerApplicationFactory::class); $app = $appFactory->create(); try { $app->runComposerCommand(['command' => 'validate'], $path); } catch (\RuntimeException $exception) { $this->fail($exception->getMessage()); } } /** * Some of coding style conventions * * @param string $contents */ private function assertCodingStyle($contents) { $this->assertDoesNotMatchRegularExpression( '/" :\s*["{]/', $contents, 'Coding style: there should be no space before colon.' ); $this->assertDoesNotMatchRegularExpression( '/":["{]/', $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('require', $json); $this->assertEquals($packageType, $json->type); if ($packageType !== 'project') { self::$dependencies[] = $json->name; $this->assertAutoloadRegistrar($json, $dir); $this->assertNoMap($json); } switch ($packageType) { case 'magento2-module': $xml = simplexml_load_file("$dir/etc/module.xml"); if ($this->isVendorMagento($json->name)) { $this->assertConsistentModuleName($xml, $json->name); } $this->assertDependsOnPhp($json->require); $this->assertPhpVersionInSync($json->name, $json->require->php); $this->assertDependsOnFramework($json->require); $this->assertRequireInSync($json); $this->assertAutoload($json); $this->assertNoVersionSpecified($json); break; case 'magento2-language': $this->assertMatchesRegularExpression( '/^magento\/language\-[a-z]{2}_([a-z]{4}_)?[a-z]{2}$/', $json->name ); $this->assertDependsOnFramework($json->require); $this->assertRequireInSync($json); $this->assertNoVersionSpecified($json); break; case 'magento2-theme': $this->assertMatchesRegularExpression( '/^magento\/theme-(?:adminhtml|frontend)(\-[a-z0-9_]+)+$/', $json->name ); $this->assertDependsOnPhp($json->require); $this->assertPhpVersionInSync($json->name, $json->require->php); $this->assertDependsOnFramework($json->require); $this->assertRequireInSync($json); $this->assertNoVersionSpecified($json); break; case 'magento2-library': $this->assertDependsOnPhp($json->require); $this->assertMatchesRegularExpression('/^magento\/framework*/', $json->name); $this->assertPhpVersionInSync($json->name, $json->require->php); $this->assertRequireInSync($json); $this->assertAutoload($json); $this->assertNoVersionSpecified($json); break; case 'project': $this->checkProject(); $this->assertNoVersionSpecified($json); break; default: throw new \InvalidArgumentException("Unknown package type {$packageType}"); } } /** * Checks if package vendor is Magento. * * @param string $packageName * @return bool */ private function isVendorMagento(string $packageName): bool { return strpos($packageName, 'magento/') === 0; } /** * Assert that component registrar is autoloaded in composer json * * @param \StdClass $json * @param string $dir */ private function assertAutoloadRegistrar(\StdClass $json, $dir) { $error = 'There must be an "autoload->files" node in composer.json of each Magento component.'; $this->assertObjectHasAttribute('autoload', $json, $error); $this->assertObjectHasAttribute('files', $json->autoload, $error); $this->assertTrue(in_array("registration.php", $json->autoload->files), $error); $this->assertFileExists("$dir/registration.php"); } /** * Version must not be specified in the root and package composer JSON files in Git. * * All versions are added by tools during release publication by version setter tool. * * @param \StdClass $json */ private function assertNoVersionSpecified(\StdClass $json) { if (!in_array($json->name, self::$rootComposerModuleBlacklist)) { $errorMessage = 'Version must not be specified in the root and package composer JSON files in Git'; $this->assertObjectNotHasAttribute('version', $json, $errorMessage); } } /** * Assert that there is PSR-4 autoload in composer json * * @param \StdClass $json */ private function assertAutoload(\StdClass $json) { $errorMessage = 'There must be an "autoload->psr-4" section in composer.json of each Magento component.'; $this->assertObjectHasAttribute('autoload', $json, $errorMessage); $this->assertObjectHasAttribute('psr-4', $json->autoload, $errorMessage); } /** * Assert that there is map in specified composer json * * @param \StdClass $json */ private function assertNoMap(\StdClass $json) { $error = 'There is no "extra->map" node in composer.json of each Magento component.'; $this->assertObjectNotHasAttribute('extra', $json, $error); } /** * Enforce package naming conventions for modules * * @param \SimpleXMLElement $xml * @param string $packageName */ private function assertConsistentModuleName(\SimpleXMLElement $xml, $packageName) { if (!in_array($packageName, self::$moduleNameBlacklist)) { $moduleName = (string)$xml->module->attributes()->name; $expectedPackageName = $this->convertModuleToPackageName($moduleName); $this->assertEquals( $expectedPackageName, $packageName, "For the module '{$moduleName}', the expected package name is '{$expectedPackageName}'" ); } } /** * 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( self::$magentoFrameworkLibraryName, $json, 'This component is expected to depend on ' . self::$magentoFrameworkLibraryName ); } /** * Assert that PHP versions in root composer.json and Magento component's composer.json are not out of sync * * @param string $name * @param string $phpVersion */ private function assertPhpVersionInSync($name, $phpVersion) { if (isset(self::$rootJson['require']['php'])) { $composerVersionsPattern = '{\s*\|\|?\s*}'; $rootPhpVersions = preg_split($composerVersionsPattern, self::$rootJson['require']['php']); $modulePhpVersions = preg_split($composerVersionsPattern, $phpVersion); $this->assertEmpty( array_diff($rootPhpVersions, $modulePhpVersions), "PHP version {$phpVersion} in component {$name} is inconsistent with version " . self::$rootJson['require']['php'] . ' in root composer.json' ); } } /** * Make sure requirements of components are reflected in root composer.json * * @param \StdClass $json * @return void */ private function assertRequireInSync(\StdClass $json) { if (preg_match('/magento\/project-*/', self::$rootJson['name']) == 1) { return; } if (!in_array($json->name, self::$rootComposerModuleBlacklist) && isset($json->require)) { $this->checkPackageInRootComposer($json); } } /** * Check if package is reflected in root composer.json * * @param \StdClass $json * @return void */ private function checkPackageInRootComposer(\StdClass $json) { $name = $json->name; $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\d][a-z]*)/', $name, -1, PREG_SPLIT_DELIM_CAPTURE) as $chunk) { $package .= $chunk ? "-{$chunk}" : ''; } return strtolower("{$vendor}/{$package}"); } 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']); foreach ($flat as $item) { list($component, $path) = $item; $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; } /** * @return void */ private function checkProject() { sort(self::$dependencies); $dependenciesListed = []; if (strpos(self::$rootJson['name'], 'magento/project-') !== 0) { $this->assertArrayHasKey( 'replace', (array)self::$rootJson, 'No "replace" section found in root composer.json' ); foreach (array_keys((array)self::$rootJson['replace']) as $key) { if (MagentoComponent::matchMagentoComponent($key)) { $dependenciesListed[] = $key; } } sort($dependenciesListed); $nonDeclaredDependencies = array_diff( self::$dependencies, $dependenciesListed, self::$rootComposerModuleBlacklist ); $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) ); } } /** * Check the correspondence between the root composer file and magento/framework composer file. */ public function testConsistencyOfDeclarationsInComposerFiles() { if (strpos(self::$rootJson['name'], 'magento/project-') !== false) { // The Dependency test is skipped for vendor/magento build self::markTestSkipped( 'The build is running for composer installation. Consistency test for composer files is skipped.' ); } $componentRegistrar = new ComponentRegistrar(); $magentoFrameworkLibraryDir = $componentRegistrar->getPath(ComponentRegistrar::LIBRARY, self::$magentoFrameworkLibraryName); $magentoFrameworkComposerFile = json_decode( file_get_contents($magentoFrameworkLibraryDir . DIRECTORY_SEPARATOR . 'composer.json'), true ); $inconsistentDependencies = []; foreach ($magentoFrameworkComposerFile['require'] as $dependency => $constraint) { if (isset(self::$rootJson['require'][$dependency]) && self::$rootJson['require'][$dependency] !== $constraint ) { $inconsistentDependencies[] = $dependency; } } $this->assertEmpty( $inconsistentDependencies, 'There is a discrepancy between the declared versions of the following modules in "' . self::$magentoFrameworkLibraryName . '" and the root composer.json: ' . implode(', ', $inconsistentDependencies) ); } }