ComposerIntegrationTest.php 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. <?php
  2. namespace Drupal\Tests;
  3. use Composer\Semver\Semver;
  4. /**
  5. * Tests Composer integration.
  6. *
  7. * @group Composer
  8. */
  9. class ComposerIntegrationTest extends UnitTestCase {
  10. /**
  11. * The minimum PHP version supported by Drupal.
  12. *
  13. * @see https://www.drupal.org/docs/8/system-requirements/web-server
  14. *
  15. * @todo Remove as part of https://www.drupal.org/node/2908079
  16. */
  17. const MIN_PHP_VERSION = '5.5.9';
  18. /**
  19. * Gets human-readable JSON error messages.
  20. *
  21. * @return string[]
  22. * Keys are JSON_ERROR_* constants.
  23. */
  24. protected function getErrorMessages() {
  25. $messages = [
  26. 0 => 'No errors found',
  27. JSON_ERROR_DEPTH => 'The maximum stack depth has been exceeded',
  28. JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON',
  29. JSON_ERROR_CTRL_CHAR => 'Control character error, possibly incorrectly encoded',
  30. JSON_ERROR_SYNTAX => 'Syntax error',
  31. JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded',
  32. ];
  33. if (version_compare(phpversion(), '5.5.0', '>=')) {
  34. $messages[JSON_ERROR_RECURSION] = 'One or more recursive references in the value to be encoded';
  35. $messages[JSON_ERROR_INF_OR_NAN] = 'One or more NAN or INF values in the value to be encoded';
  36. $messages[JSON_ERROR_UNSUPPORTED_TYPE] = 'A value of a type that cannot be encoded was given';
  37. }
  38. return $messages;
  39. }
  40. /**
  41. * Gets the paths to the folders that contain the Composer integration.
  42. *
  43. * @return string[]
  44. * The paths.
  45. */
  46. protected function getPaths() {
  47. return [
  48. $this->root,
  49. $this->root . '/core',
  50. $this->root . '/core/lib/Drupal/Component/Annotation',
  51. $this->root . '/core/lib/Drupal/Component/Assertion',
  52. $this->root . '/core/lib/Drupal/Component/Bridge',
  53. $this->root . '/core/lib/Drupal/Component/ClassFinder',
  54. $this->root . '/core/lib/Drupal/Component/Datetime',
  55. $this->root . '/core/lib/Drupal/Component/DependencyInjection',
  56. $this->root . '/core/lib/Drupal/Component/Diff',
  57. $this->root . '/core/lib/Drupal/Component/Discovery',
  58. $this->root . '/core/lib/Drupal/Component/EventDispatcher',
  59. $this->root . '/core/lib/Drupal/Component/FileCache',
  60. $this->root . '/core/lib/Drupal/Component/FileSystem',
  61. $this->root . '/core/lib/Drupal/Component/Gettext',
  62. $this->root . '/core/lib/Drupal/Component/Graph',
  63. $this->root . '/core/lib/Drupal/Component/HttpFoundation',
  64. $this->root . '/core/lib/Drupal/Component/PhpStorage',
  65. $this->root . '/core/lib/Drupal/Component/Plugin',
  66. $this->root . '/core/lib/Drupal/Component/ProxyBuilder',
  67. $this->root . '/core/lib/Drupal/Component/Render',
  68. $this->root . '/core/lib/Drupal/Component/Serialization',
  69. $this->root . '/core/lib/Drupal/Component/Transliteration',
  70. $this->root . '/core/lib/Drupal/Component/Utility',
  71. $this->root . '/core/lib/Drupal/Component/Uuid',
  72. ];
  73. }
  74. /**
  75. * Tests composer.json.
  76. */
  77. public function testComposerJson() {
  78. foreach ($this->getPaths() as $path) {
  79. $json = file_get_contents($path . '/composer.json');
  80. $result = json_decode($json);
  81. $this->assertNotNull($result, $this->getErrorMessages()[json_last_error()]);
  82. }
  83. }
  84. /**
  85. * Tests composer.lock content-hash.
  86. */
  87. public function testComposerLockHash() {
  88. $content_hash = self::getContentHash(file_get_contents($this->root . '/composer.json'));
  89. $lock = json_decode(file_get_contents($this->root . '/composer.lock'), TRUE);
  90. $this->assertSame($content_hash, $lock['content-hash']);
  91. }
  92. /**
  93. * Tests composer.json versions.
  94. *
  95. * @param string $path
  96. * Path to a composer.json to test.
  97. *
  98. * @dataProvider providerTestComposerJson
  99. */
  100. public function testComposerTilde($path) {
  101. $content = json_decode(file_get_contents($path), TRUE);
  102. $composer_keys = array_intersect(['require', 'require-dev'], array_keys($content));
  103. if (empty($composer_keys)) {
  104. $this->markTestSkipped("$path has no keys to test");
  105. }
  106. foreach ($composer_keys as $composer_key) {
  107. foreach ($content[$composer_key] as $dependency => $version) {
  108. // We allow tildes if the dependency is a Symfony component.
  109. // @see https://www.drupal.org/node/2887000
  110. if (strpos($dependency, 'symfony/') === 0) {
  111. continue;
  112. }
  113. $this->assertFalse(strpos($version, '~'), "Dependency $dependency in $path contains a tilde, use a caret.");
  114. }
  115. }
  116. }
  117. /**
  118. * Data provider for all the composer.json provided by Drupal core.
  119. *
  120. * @return array
  121. */
  122. public function providerTestComposerJson() {
  123. $root = realpath(__DIR__ . '/../../../../');
  124. $tests = [[$root . '/composer.json']];
  125. $directory = new \RecursiveDirectoryIterator($root . '/core');
  126. $iterator = new \RecursiveIteratorIterator($directory);
  127. /** @var \SplFileInfo $file */
  128. foreach ($iterator as $file) {
  129. if ($file->getFilename() === 'composer.json' && strpos($file->getPath(), 'core/modules/system/tests/fixtures/HtaccessTest') === FALSE) {
  130. $tests[] = [$file->getRealPath()];
  131. }
  132. }
  133. return $tests;
  134. }
  135. /**
  136. * Tests core's composer.json replace section.
  137. *
  138. * Verify that all core modules are also listed in the 'replace' section of
  139. * core's composer.json.
  140. */
  141. public function testAllModulesReplaced() {
  142. // Assemble a path to core modules.
  143. $module_path = $this->root . '/core/modules';
  144. // Grab the 'replace' section of the core composer.json file.
  145. $json = json_decode(file_get_contents($this->root . '/core/composer.json'));
  146. $composer_replace_packages = (array) $json->replace;
  147. // Get a list of all the files in the module path.
  148. $folders = scandir($module_path);
  149. // Make sure we only deal with directories that aren't . or ..
  150. $module_names = [];
  151. $discard = ['.', '..'];
  152. foreach ($folders as $file_name) {
  153. if ((!in_array($file_name, $discard)) && is_dir($module_path . '/' . $file_name)) {
  154. $module_names[] = $file_name;
  155. }
  156. }
  157. // Assert that each core module has a corresponding 'replace' in
  158. // composer.json.
  159. foreach ($module_names as $module_name) {
  160. $this->assertArrayHasKey(
  161. 'drupal/' . $module_name,
  162. $composer_replace_packages,
  163. 'Unable to find ' . $module_name . ' in replace list of composer.json'
  164. );
  165. }
  166. }
  167. /**
  168. * Tests package requirements for the minimum supported PHP version by Drupal.
  169. *
  170. * @todo This can be removed when DrupalCI supports dependency regression
  171. * testing in https://www.drupal.org/node/2874198
  172. */
  173. public function testMinPHPVersion() {
  174. // Check for lockfile in the application root. If the lockfile does not
  175. // exist, then skip this test.
  176. $lockfile = $this->root . '/composer.lock';
  177. if (!file_exists($lockfile)) {
  178. $this->markTestSkipped('/composer.lock is not available.');
  179. }
  180. $lock = json_decode(file_get_contents($lockfile), TRUE);
  181. // Check the PHP version for each installed non-development package. The
  182. // testing infrastructure uses the uses the development packages, and may
  183. // update them for particular environment configurations. In particular,
  184. // PHP 7.2+ require an updated version of phpunit, which is incompatible
  185. // with Drupal's minimum PHP requirement.
  186. foreach ($lock['packages'] as $package) {
  187. if (isset($package['require']['php'])) {
  188. $this->assertTrue(Semver::satisfies(static::MIN_PHP_VERSION, $package['require']['php']), $package['name'] . ' has a PHP dependency requirement of "' . $package['require']['php'] . '"');
  189. }
  190. }
  191. }
  192. // @codingStandardsIgnoreStart
  193. /**
  194. * The following method is copied from \Composer\Package\Locker.
  195. *
  196. * @see https://github.com/composer/composer
  197. */
  198. /**
  199. * Returns the md5 hash of the sorted content of the composer file.
  200. *
  201. * @param string $composerFileContents The contents of the composer file.
  202. *
  203. * @return string
  204. */
  205. protected static function getContentHash($composerFileContents)
  206. {
  207. $content = json_decode($composerFileContents, true);
  208. $relevantKeys = array(
  209. 'name',
  210. 'version',
  211. 'require',
  212. 'require-dev',
  213. 'conflict',
  214. 'replace',
  215. 'provide',
  216. 'minimum-stability',
  217. 'prefer-stable',
  218. 'repositories',
  219. 'extra',
  220. );
  221. $relevantContent = array();
  222. foreach (array_intersect($relevantKeys, array_keys($content)) as $key) {
  223. $relevantContent[$key] = $content[$key];
  224. }
  225. if (isset($content['config']['platform'])) {
  226. $relevantContent['config']['platform'] = $content['config']['platform'];
  227. }
  228. ksort($relevantContent);
  229. return md5(json_encode($relevantContent));
  230. }
  231. // @codingStandardsIgnoreEnd
  232. }