Magento 2 Documentation  2.3
Documentation for Magento 2 CMS v2.3 (December 2018)
ComposerTest.php
Go to the documentation of this file.
1 <?php
6 namespace Magento\Test\Integrity;
7 
11 
15 class ComposerTest extends \PHPUnit\Framework\TestCase
16 {
17 
21  private static $root;
22 
26  private static $rootJson;
27 
31  private static $dependencies;
32 
36  private static $objectManager;
37 
41  private static $rootComposerModuleBlacklist = [];
42 
46  private static $moduleNameBlacklist;
47 
48  public static function setUpBeforeClass()
49  {
50  self::$root = BP;
51  self::$rootJson = json_decode(file_get_contents(self::$root . '/composer.json'), true);
52  self::$dependencies = [];
53  self::$objectManager = Bootstrap::create(BP, $_SERVER)->getObjectManager();
54  // A block can be whitelisted and thus not be required to be public
55  self::$rootComposerModuleBlacklist = self::getBlacklist(
56  __DIR__ . '/_files/blacklist/composer_root_modules*.txt'
57  );
58  self::$moduleNameBlacklist = self::getBlacklist(__DIR__ . '/_files/blacklist/composer_module_names*.txt');
59  }
60 
67  public static function getBlacklist(string $pattern)
68  {
69  $blacklist = [];
70  foreach (glob($pattern) as $list) {
71  $blacklist = array_merge($blacklist, file($list, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES));
72  }
73  return $blacklist;
74  }
75 
76  public function testValidComposerJson()
77  {
78  $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this);
79  $invoker(
84  function ($dir, $packageType) {
85  $file = $dir . '/composer.json';
86  $this->assertFileExists($file);
87  $this->validateComposerJsonFile($dir);
89  $json = json_decode($contents);
90  $this->assertCodingStyle($contents);
91  $this->assertMagentoConventions($dir, $packageType, $json);
92  },
94  );
95  }
96 
101  {
102  $root = BP;
104  $result = [];
105  foreach ($componentRegistrar->getPaths(ComponentRegistrar::MODULE) as $dir) {
106  $result[$dir] = [$dir, 'magento2-module'];
107  }
108  foreach ($componentRegistrar->getPaths(ComponentRegistrar::LANGUAGE) as $dir) {
109  $result[$dir] = [$dir, 'magento2-language'];
110  }
111  foreach ($componentRegistrar->getPaths(ComponentRegistrar::THEME) as $dir) {
112  $result[$dir] = [$dir, 'magento2-theme'];
113  }
114  foreach ($componentRegistrar->getPaths(ComponentRegistrar::LIBRARY) as $dir) {
115  $result[$dir] = [$dir, 'magento2-library'];
116  }
117  $result[$root] = [$root, 'project'];
118 
119  return $result;
120  }
121 
127  private function validateComposerJsonFile($path)
128  {
130  $appFactory = self::$objectManager->get(\Magento\Framework\Composer\MagentoComposerApplicationFactory::class);
131  $app = $appFactory->create();
132 
133  try {
134  $app->runComposerCommand(['command' => 'validate'], $path);
135  } catch (\RuntimeException $exception) {
136  $this->fail($exception->getMessage());
137  }
138  }
139 
145  private function assertCodingStyle($contents)
146  {
147  $this->assertNotRegExp('/" :\s*["{]/', $contents, 'Coding style: there should be no space before colon.');
148  $this->assertNotRegExp('/":["{]/', $contents, 'Coding style: a space is necessary after colon.');
149  }
150 
159  private function assertMagentoConventions($dir, $packageType, \StdClass $json)
160  {
161  $this->assertObjectHasAttribute('name', $json);
162  $this->assertObjectHasAttribute('license', $json);
163  $this->assertObjectHasAttribute('type', $json);
164  $this->assertObjectHasAttribute('require', $json);
165  $this->assertEquals($packageType, $json->type);
166  if ($packageType !== 'project') {
167  self::$dependencies[] = $json->name;
168  $this->assertAutoloadRegistrar($json, $dir);
169  $this->assertNoMap($json);
170  }
171  switch ($packageType) {
172  case 'magento2-module':
173  $xml = simplexml_load_file("$dir/etc/module.xml");
174  if ($this->isVendorMagento($json->name)) {
175  $this->assertConsistentModuleName($xml, $json->name);
176  }
177  $this->assertDependsOnPhp($json->require);
178  $this->assertPhpVersionInSync($json->name, $json->require->php);
179  $this->assertDependsOnFramework($json->require);
180  $this->assertRequireInSync($json);
181  $this->assertAutoload($json);
182  $this->assertNoVersionSpecified($json);
183  break;
184  case 'magento2-language':
185  $this->assertRegExp('/^magento\/language\-[a-z]{2}_([a-z]{4}_)?[a-z]{2}$/', $json->name);
186  $this->assertDependsOnFramework($json->require);
187  $this->assertRequireInSync($json);
188  $this->assertNoVersionSpecified($json);
189  break;
190  case 'magento2-theme':
191  $this->assertRegExp('/^magento\/theme-(?:adminhtml|frontend)(\-[a-z0-9_]+)+$/', $json->name);
192  $this->assertDependsOnPhp($json->require);
193  $this->assertPhpVersionInSync($json->name, $json->require->php);
194  $this->assertDependsOnFramework($json->require);
195  $this->assertRequireInSync($json);
196  $this->assertNoVersionSpecified($json);
197  break;
198  case 'magento2-library':
199  $this->assertDependsOnPhp($json->require);
200  $this->assertRegExp('/^magento\/framework*/', $json->name);
201  $this->assertPhpVersionInSync($json->name, $json->require->php);
202  $this->assertRequireInSync($json);
203  $this->assertAutoload($json);
204  $this->assertNoVersionSpecified($json);
205  break;
206  case 'project':
207  $this->checkProject();
208  $this->assertNoVersionSpecified($json);
209  break;
210  default:
211  throw new \InvalidArgumentException("Unknown package type {$packageType}");
212  }
213  }
214 
221  private function isVendorMagento(string $packageName): bool
222  {
223  return strpos($packageName, 'magento/') === 0;
224  }
225 
232  private function assertAutoloadRegistrar(\StdClass $json, $dir)
233  {
234  $error = 'There must be an "autoload->files" node in composer.json of each Magento component.';
235  $this->assertObjectHasAttribute('autoload', $json, $error);
236  $this->assertObjectHasAttribute('files', $json->autoload, $error);
237  $this->assertTrue(in_array("registration.php", $json->autoload->files), $error);
238  $this->assertFileExists("$dir/registration.php");
239  }
240 
248  private function assertNoVersionSpecified(\StdClass $json)
249  {
250  $errorMessage = 'Version must not be specified in the root and package composer JSON files in Git';
251  $this->assertObjectNotHasAttribute('version', $json, $errorMessage);
252  }
253 
259  private function assertAutoload(\StdClass $json)
260  {
261  $errorMessage = 'There must be an "autoload->psr-4" section in composer.json of each Magento component.';
262  $this->assertObjectHasAttribute('autoload', $json, $errorMessage);
263  $this->assertObjectHasAttribute('psr-4', $json->autoload, $errorMessage);
264  }
265 
271  private function assertNoMap(\StdClass $json)
272  {
273  $error = 'There is no "extra->map" node in composer.json of each Magento component.';
274  $this->assertObjectNotHasAttribute('extra', $json, $error);
275  }
276 
283  private function assertConsistentModuleName(\SimpleXMLElement $xml, $packageName)
284  {
285  if (!in_array($packageName, self::$moduleNameBlacklist)) {
286  $moduleName = (string)$xml->module->attributes()->name;
287  $expectedPackageName = $this->convertModuleToPackageName($moduleName);
288  $this->assertEquals(
289  $expectedPackageName,
290  $packageName,
291  "For the module '{$moduleName}', the expected package name is '{$expectedPackageName}'"
292  );
293  }
294  }
295 
301  private function assertDependsOnPhp(\StdClass $json)
302  {
303  $this->assertObjectHasAttribute('php', $json, 'This component is expected to depend on certain PHP version(s)');
304  }
305 
311  private function assertDependsOnFramework(\StdClass $json)
312  {
313  $this->assertObjectHasAttribute(
314  'magento/framework',
315  $json,
316  'This component is expected to depend on magento/framework'
317  );
318  }
319 
326  private function assertPhpVersionInSync($name, $phpVersion)
327  {
328  if (isset(self::$rootJson['require']['php'])) {
329  if ($this->isVendorMagento($name)) {
330  $this->assertEquals(
331  self::$rootJson['require']['php'],
332  $phpVersion,
333  "PHP version {$phpVersion} in component {$name} is inconsistent with version "
334  . self::$rootJson['require']['php'] . ' in root composer.json'
335  );
336  } else {
337  $composerVersionsPattern = '{\s*\|\|?\s*}';
338  $rootPhpVersions = preg_split($composerVersionsPattern, self::$rootJson['require']['php']);
339  $modulePhpVersions = preg_split($composerVersionsPattern, $phpVersion);
340 
341  $this->assertEmpty(
342  array_diff($rootPhpVersions, $modulePhpVersions),
343  "PHP version {$phpVersion} in component {$name} is inconsistent with version "
344  . self::$rootJson['require']['php'] . ' in root composer.json'
345  );
346  }
347  }
348  }
349 
356  private function assertRequireInSync(\StdClass $json)
357  {
358  if (preg_match('/magento\/project-*/', self::$rootJson['name']) == 1) {
359  return;
360  }
361  if (!in_array($json->name, self::$rootComposerModuleBlacklist) && isset($json->require)) {
362  $this->checkPackageInRootComposer($json);
363  }
364  }
365 
372  private function checkPackageInRootComposer(\StdClass $json)
373  {
374  $name = $json->name;
375  $errors = [];
376  foreach (array_keys((array)$json->require) as $depName) {
377  if ($depName == 'magento/magento-composer-installer') {
378  // Magento Composer Installer is not needed for already existing components
379  continue;
380  }
381  if (!isset(self::$rootJson['require-dev'][$depName]) && !isset(self::$rootJson['require'][$depName])
382  && !isset(self::$rootJson['replace'][$depName])) {
383  $errors[] = "'$name' depends on '$depName'";
384  }
385  }
386  if (!empty($errors)) {
387  $this->fail(
388  "The following dependencies are missing in root 'composer.json',"
389  . " while declared in child components.\n"
390  . "Consider adding them to 'require-dev' section (if needed for child components only),"
391  . " to 'replace' section (if they are present in the project),"
392  . " to 'require' section (if needed for the skeleton).\n"
393  . join("\n", $errors)
394  );
395  }
396  }
397 
404  private function convertModuleToPackageName($moduleName)
405  {
406  list($vendor, $name) = explode('_', $moduleName, 2);
407  $package = 'module';
408  foreach (preg_split('/([A-Z\d][a-z]*)/', $name, -1, PREG_SPLIT_DELIM_CAPTURE) as $chunk) {
409  $package .= $chunk ? "-{$chunk}" : '';
410  }
411  return strtolower("{$vendor}/{$package}");
412  }
413 
414  public function testComponentPathsInRoot()
415  {
416  if (!isset(self::$rootJson['extra']) || !isset(self::$rootJson['extra']['component_paths'])) {
417  $this->markTestSkipped("The root composer.json file doesn't mention any extra component paths information");
418  }
419  $this->assertArrayHasKey(
420  'replace',
421  self::$rootJson,
422  "If there are any component paths specified, then they must be reflected in 'replace' section"
423  );
424  $flat = $this->getFlatPathsInfo(self::$rootJson['extra']['component_paths']);
425  foreach ($flat as $item) {
426  list($component, $path) = $item;
427  $this->assertFileExists(
428  self::$root . '/' . $path,
429  "Missing or invalid component path: {$component} -> {$path}"
430  );
431  $this->assertArrayHasKey(
432  $component,
433  self::$rootJson['replace'],
434  "The {$component} is specified in 'extra->component_paths', but missing in 'replace' section"
435  );
436  }
437  foreach (array_keys(self::$rootJson['replace']) as $replace) {
439  $this->assertArrayHasKey(
440  $replace,
441  self::$rootJson['extra']['component_paths'],
442  "The {$replace} is specified in 'replace', but missing in 'extra->component_paths' section"
443  );
444  }
445  }
446  }
447 
453  private function getFlatPathsInfo(array $info)
454  {
455  $flat = [];
456  foreach ($info as $key => $element) {
457  if (is_string($element)) {
458  $flat[] = [$key, $element];
459  } elseif (is_array($element)) {
460  foreach ($element as $path) {
461  $flat[] = [$key, $path];
462  }
463  } else {
464  throw new \Exception("Unexpected element 'in extra->component_paths' section");
465  }
466  }
467 
468  return $flat;
469  }
470 
474  private function checkProject()
475  {
476  sort(self::$dependencies);
477  $dependenciesListed = [];
478  if (strpos(self::$rootJson['name'], 'magento/project-') !== 0) {
479  $this->assertArrayHasKey(
480  'replace',
481  (array)self::$rootJson,
482  'No "replace" section found in root composer.json'
483  );
484  foreach (array_keys((array)self::$rootJson['replace']) as $key) {
486  $dependenciesListed[] = $key;
487  }
488  }
489  sort($dependenciesListed);
490  $nonDeclaredDependencies = array_diff(
491  self::$dependencies,
492  $dependenciesListed,
493  self::$rootComposerModuleBlacklist
494  );
495  $nonexistentDependencies = array_diff($dependenciesListed, self::$dependencies);
496  $this->assertEmpty(
497  $nonDeclaredDependencies,
498  'Following dependencies are not declared in the root composer.json: '
499  . join(', ', $nonDeclaredDependencies)
500  );
501  $this->assertEmpty(
502  $nonexistentDependencies,
503  'Following dependencies declared in the root composer.json do not exist: '
504  . join(', ', $nonexistentDependencies)
505  );
506  }
507  }
508 }
static create($rootDir, array $initParams, ObjectManagerFactory $factory=null)
Definition: Bootstrap.php:119
$contents
Definition: website.php:14
$componentRegistrar
Definition: bootstrap.php:23
elseif(isset( $params[ 'redirect_parent']))
Definition: iframe.phtml:17
$objectManager
Definition: bootstrap.php:17
$pattern
Definition: website.php:22
defined('TESTS_BP')||define('TESTS_BP' __DIR__
Definition: _bootstrap.php:60
static getBlacklist(string $pattern)
const BP
Definition: autoload.php:14
$app
Definition: index.php:38
foreach( $_productCollection as $_product)() ?>" class $info
Definition: listing.phtml:52
$errors
Definition: overview.phtml:9
if(!isset($_GET['name'])) $name
Definition: log.php:14
$element
Definition: element.phtml:12