From 39bf8c10fee9beddd1ad28d44880544c60e695b1 Mon Sep 17 00:00:00 2001 From: lucashedding <lucashedding@1463982.no-reply.drupal.org> Date: Wed, 12 Jun 2019 08:25:52 -0500 Subject: [PATCH] Issue #3054002 by heddn, catch: Parse project name and version from composer.json --- composer.json | 6 +- drupalci.yml | 2 + src/ProjectInfoTrait.php | 108 +++++++++++++++ src/ReadinessChecker/MissingProjectInfo.php | 9 +- src/ReadinessChecker/ModifiedFiles.php | 2 +- src/Services/ModifiedFiles.php | 39 +----- tests/src/Kernel/ProjectInfoTraitTest.php | 146 ++++++++++++++++++++ 7 files changed, 268 insertions(+), 44 deletions(-) create mode 100644 src/ProjectInfoTrait.php create mode 100644 tests/src/Kernel/ProjectInfoTraitTest.php diff --git a/composer.json b/composer.json index 0b5f48fa1f..58ff78dd10 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "type": "drupal-module", "description": "Drupal Automatic Updates", "keywords": ["Drupal"], - "license": "GPL-2.0+", + "license": "GPL-2.0-or-later", "homepage": "https://www.drupal.org/project/automatic_updates", "minimum-stability": "dev", "support": { @@ -13,6 +13,10 @@ "require": { "ext-json": "*", "composer/semver": "^1.0", + "ocramius/package-versions": "^1.4", "webflo/drupal-finder": "^1.1" + }, + "require-dev": { + "drupal/ctools": "3.2.0" } } diff --git a/drupalci.yml b/drupalci.yml index 7286c3ba56..8fa0ea2178 100644 --- a/drupalci.yml +++ b/drupalci.yml @@ -10,6 +10,8 @@ build: sniff-all-files: true halt-on-fail: true testing: + container_command: + commands: "cd ${SOURCE_DIR} && sudo -u www-data composer require drupal/ctools:3.2.0 --prefer-source --optimize-autoloader" # run_tests task is executed several times in order of performance speeds. # halt-on-fail can be set on the run_tests tasks in order to fail fast. # suppress-deprecations is false in order to be alerted to usages of diff --git a/src/ProjectInfoTrait.php b/src/ProjectInfoTrait.php new file mode 100644 index 0000000000..1fde5d6b42 --- /dev/null +++ b/src/ProjectInfoTrait.php @@ -0,0 +1,108 @@ +<?php + +namespace Drupal\automatic_updates; + +use PackageVersions\Versions; + +/** + * Provide a helper to get project info. + */ +trait ProjectInfoTrait { + + /** + * Get the extension version. + * + * @param string $extension_name + * The extension name. + * @param array $info + * The extension's info. + * + * @return string|null + * The version or NULL if undefined. + */ + protected function getExtensionVersion($extension_name, array $info) { + if (isset($info['version']) && strpos($info['version'], '-dev') === FALSE) { + return $info['version']; + } + $composer_json = $this->getComposerJson($extension_name, $info); + $extension_name = isset($composer_json['name']) ? $composer_json['name'] : $extension_name; + try { + $version = Versions::getVersion($extension_name); + $version = $this->getSuffix($version, '@', $version); + // If we do not have a core compatibility tagged git branch, we're + // dealing with a dev-master branch that cannot be updated in place. + return substr($version, 0, 3) === \Drupal::CORE_COMPATIBILITY ? $version : NULL; + } + catch (\OutOfBoundsException $exception) { + \Drupal::logger('automatic_updates')->error('Version cannot be located for @extension', ['@extension' => $extension_name]); + } + } + + /** + * Get the extension's project name. + * + * @param string $extension_name + * The extension name. + * @param array $info + * The extension's info. + * + * @return string + * The project name or fallback to extension name if project is undefined. + */ + protected function getProjectName($extension_name, array $info) { + $project_name = $extension_name; + if (isset($info['project'])) { + $project_name = $info['project']; + } + elseif ($composer_json = $this->getComposerJson($extension_name, $info)) { + if (isset($composer_json['name'])) { + $project_name = $this->getSuffix($composer_json['name'], '/', $extension_name); + } + } + return $project_name; + } + + /** + * Get string suffix. + * + * @param string $string + * The string to parse. + * @param string $needle + * The needle. + * @param string $default + * The default value. + * + * @return string + * The sub string. + */ + protected function getSuffix($string, $needle, $default) { + $pos = strrpos($string, $needle); + return $pos === FALSE ? $default : substr($string, ++$pos); + } + + /** + * Get the composer.json as a JSON array. + * + * @param string $extension_name + * The extension name. + * @param array $info + * The extension's info. + * + * @return array|null + * The composer.json as an array or NULL. + */ + protected function getComposerJson($extension_name, array $info) { + try { + if ($directory = drupal_get_path($info['type'], $extension_name)) { + $composer_json = $directory . DIRECTORY_SEPARATOR . 'composer.json'; + if (file_exists($composer_json)) { + return json_decode(file_get_contents($composer_json), TRUE); + } + } + } + catch (\Throwable $exception) { + \Drupal::logger('automatic_updates')->error('Composer.json could not be located for @extension', ['@extension' => $extension_name]); + } + } + +} diff --git a/src/ReadinessChecker/MissingProjectInfo.php b/src/ReadinessChecker/MissingProjectInfo.php index e36d7df90c..a1599ee4c8 100644 --- a/src/ReadinessChecker/MissingProjectInfo.php +++ b/src/ReadinessChecker/MissingProjectInfo.php @@ -3,6 +3,7 @@ namespace Drupal\automatic_updates\ReadinessChecker; use Drupal\automatic_updates\IgnoredPathsTrait; +use Drupal\automatic_updates\ProjectInfoTrait; use Drupal\Core\Extension\ExtensionList; use Drupal\Core\StringTranslation\StringTranslationTrait; use DrupalFinder\DrupalFinder; @@ -12,6 +13,7 @@ use DrupalFinder\DrupalFinder; */ class MissingProjectInfo extends Filesystem { use IgnoredPathsTrait; + use ProjectInfoTrait; use StringTranslationTrait; /** @@ -74,11 +76,8 @@ class MissingProjectInfo extends Filesystem { if ($this->isIgnoredPath(drupal_get_path($info['type'], $extension_name))) { continue; } - if (empty($info['version'])) { - $messages[] = $this->t('The project "@extension" will not be updated because it is missing the "version" key in the @extension.info.yml file.', ['@extension' => $extension_name]); - } - if (empty($info['project'])) { - $messages[] = $this->t('The project "@extension" will not be updated because it is missing the "project" key in the @extension.info.yml file.', ['@extension' => $extension_name]); + if (!$this->getExtensionVersion($extension_name, $info)) { + $messages[] = $this->t('The project "@extension" can not be updated because its version is either undefined or a dev release.', ['@extension' => $extension_name]); } } } diff --git a/src/ReadinessChecker/ModifiedFiles.php b/src/ReadinessChecker/ModifiedFiles.php index 3991254369..2d2b516d62 100644 --- a/src/ReadinessChecker/ModifiedFiles.php +++ b/src/ReadinessChecker/ModifiedFiles.php @@ -83,7 +83,7 @@ class ModifiedFiles implements ReadinessCheckerInterface { protected function modifiedFilesCheck() { $messages = []; $extensions = []; - $extensions['drupal'] = $this->modules->get('system')->info; + $extensions['system'] = $this->modules->get('system')->info; foreach ($this->getExtensionsTypes() as $extension_type) { foreach ($this->getInfos($extension_type) as $extension_name => $info) { if (substr($this->getPath($extension_type, $extension_name), 0, 4) !== 'core') { diff --git a/src/Services/ModifiedFiles.php b/src/Services/ModifiedFiles.php index 7d2d13ba7f..41f5088508 100644 --- a/src/Services/ModifiedFiles.php +++ b/src/Services/ModifiedFiles.php @@ -3,6 +3,7 @@ namespace Drupal\automatic_updates\Services; use Drupal\automatic_updates\IgnoredPathsTrait; +use Drupal\automatic_updates\ProjectInfoTrait; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Url; use DrupalFinder\DrupalFinder; @@ -16,6 +17,7 @@ use Psr\Log\LoggerInterface; */ class ModifiedFiles implements ModifiedFilesInterface { use IgnoredPathsTrait; + use ProjectInfoTrait; /** * The logger. @@ -173,43 +175,6 @@ class ModifiedFiles implements ModifiedFilesInterface { return Url::fromUri($uri . "/$project_name/$version/$hash_name")->toString(); } - /** - * Get the extension version. - * - * @param string $extension_name - * The extension name. - * @param array $info - * The extension's info. - * - * @return string|null - * The version or NULL if undefined. - */ - protected function getExtensionVersion($extension_name, array $info) { - $version = isset($info['version']) ? $info['version'] : NULL; - // TODO: consider using ocramius/package-versions to discover the installed - // version from composer.lock. - // See https://www.drupal.org/project/automatic_updates/issues/3054002 - return $version; - } - - /** - * Get the extension's project name. - * - * @param string $extension_name - * The extension name. - * @param array $info - * The extension's info. - * - * @return string - * The project name or fallback to extension name if project is undefined. - */ - protected function getProjectName($extension_name, array $info) { - $project_name = isset($info['project']) ? $info['project'] : $extension_name; - // TODO: parse the composer.json for the name if it isn't set in info. - // See https://www.drupal.org/project/automatic_updates/issues/3054002. - return $project_name; - } - /** * Get the hash file name. * diff --git a/tests/src/Kernel/ProjectInfoTraitTest.php b/tests/src/Kernel/ProjectInfoTraitTest.php new file mode 100644 index 0000000000..6c961db607 --- /dev/null +++ b/tests/src/Kernel/ProjectInfoTraitTest.php @@ -0,0 +1,146 @@ +<?php + +namespace Drupal\Tests\automatic_updates\Kernel; + +use Drupal\automatic_updates\ProjectInfoTrait; +use Drupal\KernelTests\KernelTestBase; + +/** + * @coversDefaultClass \Drupal\automatic_updates\ProjectInfoTrait + * @group automatic_updates + */ +class ProjectInfoTraitTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + public static $modules = [ + 'automatic_updates', + ]; + + /** + * @covers ::getExtensionVersion + * @covers ::getProjectName + * @dataProvider providerInfos + */ + public function testTrait($expected, $info, $extension_name) { + $class = new ProjectInfoTestClass(); + $this->assertSame($expected['version'], $class->getExtensionVersion($extension_name, $info)); + $this->assertSame($expected['project'], $class->getProjectName($extension_name, $info)); + } + + /** + * Data provider for testTrait. + */ + public function providerInfos() { + $infos['node']['expected'] = [ + 'version' => NULL, + 'project' => 'drupal', + ]; + $infos['node']['info'] = [ + 'name' => 'Node', + 'type' => 'module', + 'description' => 'Allows content to be submitted to the site and displayed on pages.', + 'package' => 'Core', + 'version' => '8.8.x-dev', + 'project' => 'drupal', + 'core' => '8.x', + 'configure' => 'entity.node_type.collection', + 'dependencies' => ['drupal:text'], + ]; + $infos['node']['extension_name'] = 'node'; + + $infos['update']['expected'] = [ + 'version' => NULL, + 'project' => 'drupal/update', + ]; + $infos['update']['info'] = [ + 'name' => 'Update manager', + 'type' => 'module', + 'description' => 'Checks for available updates, and can securely install or update modules and themes via a web interface.', + 'package' => 'Core', + 'core' => '8.x', + 'configure' => 'update.settings', + 'dependencies' => ['file'], + ]; + $infos['update']['extension_name'] = 'drupal/update'; + + $infos['system']['expected'] = [ + 'version' => '8.8.0', + 'project' => 'drupal', + ]; + $infos['system']['info'] = [ + 'name' => 'System', + 'type' => 'module', + 'description' => 'Handles general site configuration for administrators.', + 'package' => 'Core', + 'version' => '8.8.0', + 'project' => 'drupal', + 'core' => '8.x', + 'required' => 'true', + 'configure' => 'system.admin_config_system', + 'dependencies' => [], + ]; + $infos['system']['extension_name'] = 'system'; + + $infos['automatic_updates']['expected'] = [ + 'version' => NULL, + 'project' => 'automatic_updates', + ]; + $infos['automatic_updates']['info'] = [ + 'name' => 'Automatic Updates', + 'type' => 'module', + 'description' => 'Display public service announcements and verify readiness for applying automatic updates to the site.', + 'package' => 'Core', + 'core' => '8.x', + 'configure' => 'automatic_updates.settings', + 'dependencies' => ['system', 'update'], + ]; + $infos['automatic_updates']['extension_name'] = 'automatic_updates'; + + // TODO: Investigate switching to this project after stable release in + // https://www.drupal.org/project/automatic_updates/issues/3061229. + $infos['ctools']['expected'] = [ + 'version' => '8.x-3.2', + 'project' => 'ctools', + ]; + $infos['ctools']['info'] = [ + 'name' => 'Chaos tool suite', + 'type' => 'module', + 'description' => 'Provides a number of utility and helper APIs for Drupal developers and site builders.', + 'package' => 'Core', + 'core' => '8.x', + 'dependencies' => ['system'], + ]; + $infos['ctools']['extension_name'] = 'ctools'; + + return $infos; + } + +} + +/** + * Class ProjectInfoTestClass. + */ +class ProjectInfoTestClass { + + use ProjectInfoTrait { + getExtensionVersion as getVersion; + getProjectName as getProject; + } + + /** + * {@inheritdoc} + */ + public function getExtensionVersion($extension_name, array $info) { + return $this->getVersion($extension_name, $info); + } + + /** + * {@inheritdoc} + */ + public function getProjectName($extension_name, array $info) { + return $this->getProject($extension_name, $info); + } + +} -- GitLab