From d2a2705c9f55ff5f7b74e3e3cac486efc25b3fbc Mon Sep 17 00:00:00 2001 From: phenaproxima <phenaproxima@205645.no-reply.drupal.org> Date: Tue, 12 Oct 2021 16:09:12 +0000 Subject: [PATCH] Issue #3238647 by phenaproxima, tedbow: Create an API for easily accessing relevant Composer information --- automatic_updates.services.yml | 21 ++- composer.json | 3 +- .../core_packages.json | 0 package_manager/src/ComposerUtility.php | 147 ++++++++++++++++++ .../src/Kernel/CorePackageManifestTest.php | 4 +- src/Event/PackagesAwareTrait.php | 11 -- src/Event/PreCommitEvent.php | 32 ++++ src/Event/PreStartEvent.php | 16 ++ src/Event/ReadinessCheckEvent.php | 16 ++ src/Event/UpdateEvent.php | 28 ++++ src/Updater.php | 65 ++------ src/Validation/ReadinessValidationManager.php | 23 +-- src/Validator/StagedProjectsValidator.php | 94 +++-------- src/Validator/UpdateVersionValidator.php | 20 +-- tests/fixtures/fake-site/composer.json | 5 + tests/fixtures/fake-site/composer.lock | 1 + .../new_project_added/active/composer.json | 1 + .../new_project_added/staged/composer.json | 1 + .../no_errors/active/composer.json | 1 + .../no_errors/composer.json | 1 + .../no_errors/staged/composer.json | 1 + .../project_removed/active/composer.json | 1 + .../project_removed/staged/composer.json | 1 + .../version_changed/active/composer.json | 1 + .../version_changed/staged/composer.json | 1 + .../Functional/ReadinessValidationTest.php | 2 +- .../DiskSpaceValidatorTest.php | 3 +- .../PendingUpdatesValidatorTest.php | 25 +-- tests/src/Traits/ValidationTestTrait.php | 4 +- .../src/Unit/StagedProjectsValidatorTest.php | 66 ++++---- 30 files changed, 380 insertions(+), 215 deletions(-) rename core_packages.json => package_manager/core_packages.json (100%) create mode 100644 package_manager/src/ComposerUtility.php rename {tests => package_manager/tests}/src/Kernel/CorePackageManifestTest.php (95%) create mode 100644 tests/fixtures/fake-site/composer.json create mode 100644 tests/fixtures/fake-site/composer.lock create mode 100644 tests/fixtures/project_staged_validation/new_project_added/active/composer.json create mode 100644 tests/fixtures/project_staged_validation/new_project_added/staged/composer.json create mode 100644 tests/fixtures/project_staged_validation/no_errors/active/composer.json create mode 100644 tests/fixtures/project_staged_validation/no_errors/composer.json create mode 100644 tests/fixtures/project_staged_validation/no_errors/staged/composer.json create mode 100644 tests/fixtures/project_staged_validation/project_removed/active/composer.json create mode 100644 tests/fixtures/project_staged_validation/project_removed/staged/composer.json create mode 100644 tests/fixtures/project_staged_validation/version_changed/active/composer.json create mode 100644 tests/fixtures/project_staged_validation/version_changed/staged/composer.json diff --git a/automatic_updates.services.yml b/automatic_updates.services.yml index f831d0741a..51d5714ee9 100644 --- a/automatic_updates.services.yml +++ b/automatic_updates.services.yml @@ -1,10 +1,23 @@ services: automatic_updates.readiness_validation_manager: class: Drupal\automatic_updates\Validation\ReadinessValidationManager - arguments: ['@keyvalue.expirable', '@datetime.time', '@automatic_updates.updater', '@event_dispatcher', 24] + arguments: + - '@keyvalue.expirable' + - '@datetime.time' + - '@automatic_updates.path_locator' + - '@event_dispatcher' + - 24 automatic_updates.updater: class: Drupal\automatic_updates\Updater - arguments: ['@state', '@string_translation','@package_manager.beginner', '@package_manager.stager', '@package_manager.cleaner', '@package_manager.committer', '@event_dispatcher', '@automatic_updates.path_locator'] + arguments: + - '@state' + - '@string_translation' + - '@package_manager.beginner' + - '@package_manager.stager' + - '@package_manager.cleaner' + - '@package_manager.committer' + - '@event_dispatcher' + - '@automatic_updates.path_locator' automatic_updates.cleaner: class: Drupal\automatic_updates\ComposerStager\Cleaner decorates: package_manager.cleaner @@ -27,12 +40,12 @@ services: - { name: event_subscriber } automatic_updates.staged_projects_validator: class: Drupal\automatic_updates\Validator\StagedProjectsValidator - arguments: ['@string_translation', '@automatic_updates.path_locator'] + arguments: + - '@string_translation' tags: - { name: event_subscriber } automatic_updates.update_version_validator: class: Drupal\automatic_updates\Validator\UpdateVersionValidator - arguments: ['@automatic_updates.updater'] tags: - { name: event_subscriber } automatic_updates.composer_executable_validator: diff --git a/composer.json b/composer.json index 27ef06bb1f..388dfabb0e 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,8 @@ }, "require": { "ext-json": "*", - "php-tuf/composer-stager": "0.2.1" + "php-tuf/composer-stager": "0.2.1", + "composer/composer": "^2" }, "config": { "platform": { diff --git a/core_packages.json b/package_manager/core_packages.json similarity index 100% rename from core_packages.json rename to package_manager/core_packages.json diff --git a/package_manager/src/ComposerUtility.php b/package_manager/src/ComposerUtility.php new file mode 100644 index 0000000000..9404cbc11f --- /dev/null +++ b/package_manager/src/ComposerUtility.php @@ -0,0 +1,147 @@ +<?php + +namespace Drupal\package_manager; + +use Composer\Composer; +use Composer\Factory; +use Composer\IO\NullIO; +use Drupal\Component\Serialization\Json; + +/** + * Defines a utility object to get information from Composer's API. + */ +class ComposerUtility { + + /** + * The Composer instance. + * + * @var \Composer\Composer + */ + protected $composer; + + /** + * The statically cached names of the Drupal core packages. + * + * @var string[] + */ + private static $corePackages; + + /** + * Constructs a new ComposerUtility object. + * + * @param \Composer\Composer $composer + * The Composer instance. + */ + public function __construct(Composer $composer) { + $this->composer = $composer; + } + + /** + * Creates a utility object using the files in a given directory. + * + * @param string $dir + * The directory that contains composer.json and composer.lock. + * + * @return \Drupal\package_manager\ComposerUtility + * The utility object. + */ + public static function createForDirectory(string $dir): self { + $io = new NullIO(); + $configuration = $dir . DIRECTORY_SEPARATOR . 'composer.json'; + + // The Composer factory requires that either the HOME or COMPOSER_HOME + // environment variables be set, so momentarily set the COMPOSER_HOME + // variable to the directory we're trying to create a Composer instance for. + // We have to do this because the Composer factory doesn't give us a way to + // pass the home directory in. + // @see \Composer\Factory::getHomeDir() + $home = getenv('COMPOSER_HOME'); + putenv("COMPOSER_HOME=$dir"); + $composer = Factory::create($io, $configuration); + putenv("COMPOSER_HOME=$home"); + + return new static($composer); + } + + /** + * Returns the canonical names of the supported core packages. + * + * @return string[] + * The canonical list of supported core package names, as listed in + * ../core_packages.json. + */ + protected static function getCorePackageList(): array { + if (self::$corePackages === NULL) { + $file = __DIR__ . '/../core_packages.json'; + assert(file_exists($file), "$file does not exist."); + + $core_packages = file_get_contents($file); + $core_packages = Json::decode($core_packages); + + assert(is_array($core_packages), "$file did not contain a list of core packages."); + self::$corePackages = $core_packages; + } + return self::$corePackages; + } + + /** + * Returns the names of the core packages required in composer.json. + * + * All packages listed in ../core_packages.json are considered core packages. + * + * @return string[] + * The names of the required core packages. + * + * @throws \LogicException + * If neither drupal/core or drupal/core-recommended are required. + * + * @todo Make this return a keyed array of packages, not just names. + */ + public function getCorePackageNames(): array { + $requirements = array_keys($this->composer->getPackage()->getRequires()); + + // Ensure that either drupal/core or drupal/core-recommended are required. + // If neither is, then core cannot be updated, which we consider an error + // condition. + // @todo Move this check to an update validator as part of + // https://www.drupal.org/project/automatic_updates/issues/3241105 + $core_requirements = array_intersect(['drupal/core', 'drupal/core-recommended'], $requirements); + if (empty($core_requirements)) { + $file = $this->composer->getConfig()->getConfigSource()->getName(); + throw new \LogicException("Drupal core does not appear to be required in $file."); + } + + return array_intersect(static::getCorePackageList(), $requirements); + } + + /** + * Returns all Drupal extension packages in the lock file. + * + * The following package types are considered Drupal extension packages: + * drupal-module, drupal-theme, drupal-custom-module, and drupal-custom-theme. + * + * @return \Composer\Package\PackageInterface[] + * All Drupal extension packages in the lock file, keyed by name. + */ + public function getDrupalExtensionPackages(): array { + $locked_packages = $this->composer->getLocker() + ->getLockedRepository(TRUE) + ->getPackages(); + + $drupal_package_types = [ + 'drupal-module', + 'drupal-theme', + 'drupal-custom-module', + 'drupal-custom-theme', + ]; + $drupal_packages = []; + foreach ($locked_packages as $package) { + if (in_array($package->getType(), $drupal_package_types, TRUE)) { + $key = $package->getName(); + $drupal_packages[$key] = $package; + } + } + return $drupal_packages; + } + +} diff --git a/tests/src/Kernel/CorePackageManifestTest.php b/package_manager/tests/src/Kernel/CorePackageManifestTest.php similarity index 95% rename from tests/src/Kernel/CorePackageManifestTest.php rename to package_manager/tests/src/Kernel/CorePackageManifestTest.php index eaa0080b42..5d82264e8a 100644 --- a/tests/src/Kernel/CorePackageManifestTest.php +++ b/package_manager/tests/src/Kernel/CorePackageManifestTest.php @@ -1,6 +1,6 @@ <?php -namespace Drupal\Tests\automatic_updates\Kernel; +namespace Drupal\Tests\package_manager\Kernel; use Drupal\Component\Serialization\Json; use Drupal\KernelTests\KernelTestBase; @@ -18,7 +18,7 @@ use Symfony\Component\Finder\Finder; * For example, the list could live in core/assets, and this test could live * in the Drupal\Tests\Composer namespace. * - * @group automatic_updates + * @group package_manager */ class CorePackageManifestTest extends KernelTestBase { diff --git a/src/Event/PackagesAwareTrait.php b/src/Event/PackagesAwareTrait.php index 2462b217ef..49ead03861 100644 --- a/src/Event/PackagesAwareTrait.php +++ b/src/Event/PackagesAwareTrait.php @@ -14,17 +14,6 @@ trait PackagesAwareTrait { */ protected $packageVersions; - /** - * Constructs a PreStartEvent. - * - * @param string[] $package_versions - * (optional) The desired package versions to update to, keyed by package - * name. - */ - public function __construct(array $package_versions = []) { - $this->packageVersions = $package_versions; - } - /** * Returns the desired package versions to update to. * diff --git a/src/Event/PreCommitEvent.php b/src/Event/PreCommitEvent.php index 1f9ed03833..1bffb85f5b 100644 --- a/src/Event/PreCommitEvent.php +++ b/src/Event/PreCommitEvent.php @@ -2,6 +2,8 @@ namespace Drupal\automatic_updates\Event; +use Drupal\package_manager\ComposerUtility; + /** * Event fired before staged changes are copied into the active site. */ @@ -9,4 +11,34 @@ class PreCommitEvent extends UpdateEvent { use ExcludedPathsTrait; + /** + * The Composer utility object for the stage directory. + * + * @var \Drupal\package_manager\ComposerUtility + */ + protected $stageComposer; + + /** + * Constructs a new PreCommitEvent object. + * + * @param \Drupal\package_manager\ComposerUtility $active_composer + * A Composer utility object for the active directory. + * @param \Drupal\package_manager\ComposerUtility $stage_composer + * A Composer utility object for the stage directory. + */ + public function __construct(ComposerUtility $active_composer, ComposerUtility $stage_composer) { + parent::__construct($active_composer); + $this->stageComposer = $stage_composer; + } + + /** + * Returns a Composer utility object for the stage directory. + * + * @return \Drupal\package_manager\ComposerUtility + * The Composer utility object for the stage directory. + */ + public function getStageComposer(): ComposerUtility { + return $this->stageComposer; + } + } diff --git a/src/Event/PreStartEvent.php b/src/Event/PreStartEvent.php index a58b422eda..d8ce9ec7be 100644 --- a/src/Event/PreStartEvent.php +++ b/src/Event/PreStartEvent.php @@ -2,6 +2,8 @@ namespace Drupal\automatic_updates\Event; +use Drupal\package_manager\ComposerUtility; + /** * Event fired before an update begins. */ @@ -10,4 +12,18 @@ class PreStartEvent extends UpdateEvent { use ExcludedPathsTrait; use PackagesAwareTrait; + /** + * Constructs a PreStartEvent object. + * + * @param \Drupal\package_manager\ComposerUtility $active_composer + * A Composer utility object for the active directory. + * @param string[] $package_versions + * (optional) The desired package versions to update to, keyed by package + * name. + */ + public function __construct(ComposerUtility $active_composer, array $package_versions = []) { + parent::__construct($active_composer); + $this->packageVersions = $package_versions; + } + } diff --git a/src/Event/ReadinessCheckEvent.php b/src/Event/ReadinessCheckEvent.php index 862a4581ca..4c165cd972 100644 --- a/src/Event/ReadinessCheckEvent.php +++ b/src/Event/ReadinessCheckEvent.php @@ -2,6 +2,8 @@ namespace Drupal\automatic_updates\Event; +use Drupal\package_manager\ComposerUtility; + /** * Event fired when checking if the site could perform an update. */ @@ -9,4 +11,18 @@ class ReadinessCheckEvent extends UpdateEvent { use PackagesAwareTrait; + /** + * Constructs a ReadinessCheckEvent object. + * + * @param \Drupal\package_manager\ComposerUtility $active_composer + * A Composer utility object for the active directory. + * @param string[] $package_versions + * (optional) The desired package versions to update to, keyed by package + * name. + */ + public function __construct(ComposerUtility $active_composer, array $package_versions = []) { + parent::__construct($active_composer); + $this->packageVersions = $package_versions; + } + } diff --git a/src/Event/UpdateEvent.php b/src/Event/UpdateEvent.php index be7eace54d..0fb3f30fbd 100644 --- a/src/Event/UpdateEvent.php +++ b/src/Event/UpdateEvent.php @@ -4,6 +4,7 @@ namespace Drupal\automatic_updates\Event; use Drupal\automatic_updates\Validation\ValidationResult; use Drupal\Component\EventDispatcher\Event; +use Drupal\package_manager\ComposerUtility; /** * Event fired when a site is updating. @@ -21,6 +22,33 @@ class UpdateEvent extends Event { */ protected $results = []; + /** + * The Composer utility object for the active directory. + * + * @var \Drupal\package_manager\ComposerUtility + */ + protected $activeComposer; + + /** + * Constructs a new UpdateEvent object. + * + * @param \Drupal\package_manager\ComposerUtility $active_composer + * A Composer utility object for the active directory. + */ + public function __construct(ComposerUtility $active_composer) { + $this->activeComposer = $active_composer; + } + + /** + * Returns a Composer utility object for the active directory. + * + * @return \Drupal\package_manager\ComposerUtility + * The Composer utility object for the active directory. + */ + public function getActiveComposer(): ComposerUtility { + return $this->activeComposer; + } + /** * Adds a validation result. * diff --git a/src/Updater.php b/src/Updater.php index 05d1d4f6a2..4cb0b90a64 100644 --- a/src/Updater.php +++ b/src/Updater.php @@ -6,10 +6,10 @@ use Drupal\automatic_updates\Event\PreCommitEvent; use Drupal\automatic_updates\Event\PreStartEvent; use Drupal\automatic_updates\Event\UpdateEvent; use Drupal\automatic_updates\Exception\UpdateException; -use Drupal\Component\Serialization\Json; use Drupal\Core\State\StateInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\StringTranslation\TranslationInterface; +use Drupal\package_manager\ComposerUtility; use Drupal\system\SystemManager; use PhpTuf\ComposerStager\Domain\BeginnerInterface; use PhpTuf\ComposerStager\Domain\CleanerInterface; @@ -141,62 +141,19 @@ class Updater { if (count($project_versions) !== 1 || !array_key_exists('drupal', $project_versions)) { throw new \InvalidArgumentException("Currently only updates to Drupal core are supported."); } + + $composer = ComposerUtility::createForDirectory($this->pathLocator->getActiveDirectory()); $packages = []; - foreach ($this->getCorePackageNames() as $package) { + foreach ($composer->getCorePackageNames() as $package) { $packages[$package] = $project_versions['drupal']; } $stage_key = $this->createActiveStage($packages); /** @var \Drupal\automatic_updates\Event\PreStartEvent $event */ - $event = $this->dispatchUpdateEvent(new PreStartEvent($packages), AutomaticUpdatesEvents::PRE_START); + $event = $this->dispatchUpdateEvent(new PreStartEvent($composer, $packages), AutomaticUpdatesEvents::PRE_START); $this->beginner->begin($this->pathLocator->getActiveDirectory(), $this->pathLocator->getStageDirectory(), $this->getExclusions($event)); return $stage_key; } - /** - * Returns the names of the core packages in the project composer.json. - * - * The following packages are considered core packages: - * - drupal/core; - * - drupal/core-recommended; - * - drupal/core-vendor-hardening; - * - drupal/core-composer-scaffold; and - * - drupal/core-project-message. - * - * @return string[] - * The names of the core packages. - * - * @throws \RuntimeException - * If the project composer.json is not found. - * @throws \LogicException - * If the project composer.json does not contain drupal/core or - * drupal/core-recommended. - * - * @todo Move this to an update validator, or use a more robust method of - * detecting the core packages. - */ - public function getCorePackageNames(): array { - $composer = realpath($this->pathLocator->getProjectRoot() . '/composer.json'); - - if (empty($composer) || !file_exists($composer)) { - throw new \RuntimeException("Could not find project-level composer.json"); - } - - $data = file_get_contents($composer); - $data = Json::decode($data); - - // Ensure that either drupal/core or drupal/core-recommended are required - // by the project. If neither is, then core will not be updated, and we - // consider that an error condition. - $requirements = array_keys($data['require']); - $core_requirements = array_intersect(['drupal/core', 'drupal/core-recommended'], $requirements); - if (empty($core_requirements)) { - throw new \LogicException("Drupal core does not appear to be required in $composer."); - } - - $list = file_get_contents(__DIR__ . '/../core_packages.json'); - return array_intersect(Json::decode($list), $requirements); - } - /** * Gets the excluded paths collected by an event object. * @@ -237,10 +194,16 @@ class Updater { * Commits the current update. */ public function commit(): void { + $active_dir = $this->pathLocator->getActiveDirectory(); + $active_composer = ComposerUtility::createForDirectory($active_dir); + + $stage_dir = $this->pathLocator->getStageDirectory(); + $stage_composer = ComposerUtility::createForDirectory($stage_dir); + /** @var \Drupal\automatic_updates\Event\PreCommitEvent $event */ - $event = $this->dispatchUpdateEvent(new PreCommitEvent(), AutomaticUpdatesEvents::PRE_COMMIT); - $this->committer->commit($this->pathLocator->getStageDirectory(), $this->pathLocator->getActiveDirectory(), $this->getExclusions($event)); - $this->dispatchUpdateEvent(new UpdateEvent(), AutomaticUpdatesEvents::POST_COMMIT); + $event = $this->dispatchUpdateEvent(new PreCommitEvent($active_composer, $stage_composer), AutomaticUpdatesEvents::PRE_COMMIT); + $this->committer->commit($stage_dir, $active_dir, $this->getExclusions($event)); + $this->dispatchUpdateEvent(new UpdateEvent($active_composer), AutomaticUpdatesEvents::POST_COMMIT); } /** diff --git a/src/Validation/ReadinessValidationManager.php b/src/Validation/ReadinessValidationManager.php index 7a69520eda..23ab82eefc 100644 --- a/src/Validation/ReadinessValidationManager.php +++ b/src/Validation/ReadinessValidationManager.php @@ -4,10 +4,11 @@ namespace Drupal\automatic_updates\Validation; use Drupal\automatic_updates\AutomaticUpdatesEvents; use Drupal\automatic_updates\Event\ReadinessCheckEvent; -use Drupal\automatic_updates\Updater; +use Drupal\automatic_updates\PathLocator; use Drupal\automatic_updates\UpdateRecommender; use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface; +use Drupal\package_manager\ComposerUtility; use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** @@ -44,11 +45,11 @@ class ReadinessValidationManager { protected $resultsTimeToLive; /** - * The updater service. + * The path locator service. * - * @var \Drupal\automatic_updates\Updater + * @var \Drupal\automatic_updates\PathLocator */ - protected $updater; + protected $pathLocator; /** * Constructs a ReadinessValidationManager. @@ -57,17 +58,17 @@ class ReadinessValidationManager { * The key/value expirable factory. * @param \Drupal\Component\Datetime\TimeInterface $time * The time service. - * @param \Drupal\automatic_updates\Updater $updater - * The updater service. + * @param \Drupal\automatic_updates\PathLocator $path_locator + * The path locator service. * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher * The event dispatcher service. * @param int $results_time_to_live * The number of hours to store results. */ - public function __construct(KeyValueExpirableFactoryInterface $key_value_expirable_factory, TimeInterface $time, Updater $updater, EventDispatcherInterface $dispatcher, int $results_time_to_live) { + public function __construct(KeyValueExpirableFactoryInterface $key_value_expirable_factory, TimeInterface $time, PathLocator $path_locator, EventDispatcherInterface $dispatcher, int $results_time_to_live) { $this->keyValueExpirable = $key_value_expirable_factory->get('automatic_updates'); $this->time = $time; - $this->updater = $updater; + $this->pathLocator = $path_locator; $this->eventDispatcher = $dispatcher; $this->resultsTimeToLive = $results_time_to_live; } @@ -78,10 +79,12 @@ class ReadinessValidationManager { * @return $this */ public function run(): self { + $composer = ComposerUtility::createForDirectory($this->pathLocator->getActiveDirectory()); + $recommender = new UpdateRecommender(); $release = $recommender->getRecommendedRelease(TRUE); if ($release) { - $core_packages = $this->updater->getCorePackageNames(); + $core_packages = $composer->getCorePackageNames(); // Update all core packages to the same version. $package_versions = array_fill(0, count($core_packages), $release->getVersion()); $package_versions = array_combine($core_packages, $package_versions); @@ -89,7 +92,7 @@ class ReadinessValidationManager { else { $package_versions = []; } - $event = new ReadinessCheckEvent($package_versions); + $event = new ReadinessCheckEvent($composer, $package_versions); $this->eventDispatcher->dispatch($event, AutomaticUpdatesEvents::READINESS_CHECK); $results = $event->getResults(); $this->keyValueExpirable->setWithExpire( diff --git a/src/Validator/StagedProjectsValidator.php b/src/Validator/StagedProjectsValidator.php index 72fa7519cb..455c965764 100644 --- a/src/Validator/StagedProjectsValidator.php +++ b/src/Validator/StagedProjectsValidator.php @@ -3,11 +3,8 @@ namespace Drupal\automatic_updates\Validator; use Drupal\automatic_updates\AutomaticUpdatesEvents; -use Drupal\automatic_updates\Event\UpdateEvent; -use Drupal\automatic_updates\Exception\UpdateException; -use Drupal\automatic_updates\PathLocator; +use Drupal\automatic_updates\Event\PreCommitEvent; use Drupal\automatic_updates\Validation\ValidationResult; -use Drupal\Component\Serialization\Json; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\StringTranslation\TranslationInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -19,81 +16,32 @@ final class StagedProjectsValidator implements EventSubscriberInterface { use StringTranslationTrait; - /** - * The path locator service. - * - * @var \Drupal\automatic_updates\PathLocator - */ - protected $pathLocator; - /** * Constructs a StagedProjectsValidation object. * * @param \Drupal\Core\StringTranslation\TranslationInterface $translation * The translation service. - * @param \Drupal\automatic_updates\PathLocator $path_locator - * The path locator service. */ - public function __construct(TranslationInterface $translation, PathLocator $path_locator) { + public function __construct(TranslationInterface $translation) { $this->setStringTranslation($translation); - $this->pathLocator = $path_locator; - } - - /** - * Gets the Drupal packages in a composer.lock file. - * - * @param string $composer_lock_file - * The composer.lock file location. - * - * @return array[] - * The Drupal packages' information, as stored in the lock file, keyed by - * package name. - */ - private function getDrupalPackagesFromLockFile(string $composer_lock_file): array { - if (!file_exists($composer_lock_file)) { - $result = ValidationResult::createError([ - $this->t("composer.lock file '@lock_file' not found.", ['@lock_file' => $composer_lock_file]), - ]); - throw new UpdateException( - [$result], - 'The staged packages could not be evaluated because composer.lock file not found.' - ); - } - $composer_lock = file_get_contents($composer_lock_file); - $drupal_packages = []; - $data = Json::decode($composer_lock); - $drupal_package_types = [ - 'drupal-module', - 'drupal-theme', - 'drupal-custom-module', - 'drupal-custom-theme', - ]; - $packages = $data['packages'] ?? []; - $packages = array_merge($packages, $data['packages-dev'] ?? []); - foreach ($packages as $package) { - if (in_array($package['type'], $drupal_package_types, TRUE)) { - $drupal_packages[$package['name']] = $package; - } - } - - return $drupal_packages; } /** * Validates the staged packages. * - * @param \Drupal\automatic_updates\Event\UpdateEvent $event - * The update event. + * @param \Drupal\automatic_updates\Event\PreCommitEvent $event + * The event object. */ - public function validateStagedProjects(UpdateEvent $event): void { + public function validateStagedProjects(PreCommitEvent $event): void { try { - $active_packages = $this->getDrupalPackagesFromLockFile($this->pathLocator->getActiveDirectory() . "/composer.lock"); - $staged_packages = $this->getDrupalPackagesFromLockFile($this->pathLocator->getStageDirectory() . "/composer.lock"); + $active_packages = $event->getActiveComposer()->getDrupalExtensionPackages(); + $staged_packages = $event->getStageComposer()->getDrupalExtensionPackages(); } - catch (UpdateException $e) { - foreach ($e->getValidationResults() as $result) { - $event->addValidationResult($result); - } + catch (\Throwable $e) { + $result = ValidationResult::createError([ + $e->getMessage(), + ]); + $event->addValidationResult($result); return; } @@ -111,8 +59,8 @@ final class StagedProjectsValidator implements EventSubscriberInterface { $new_packages_messages[] = $this->t( "@type '@name' installed.", [ - '@type' => $type_map[$new_package['type']], - '@name' => $new_package['name'], + '@type' => $type_map[$new_package->getType()], + '@name' => $new_package->getName(), ] ); } @@ -131,8 +79,8 @@ final class StagedProjectsValidator implements EventSubscriberInterface { $removed_packages_messages[] = $this->t( "@type '@name' removed.", [ - '@type' => $type_map[$removed_package['type']], - '@name' => $removed_package['name'], + '@type' => $type_map[$removed_package->getType()], + '@name' => $removed_package->getName(), ] ); } @@ -149,14 +97,14 @@ final class StagedProjectsValidator implements EventSubscriberInterface { if ($pre_existing_packages = array_diff_key($staged_packages, $removed_packages, $new_packages)) { foreach ($pre_existing_packages as $package_name => $staged_existing_package) { $active_package = $active_packages[$package_name]; - if ($staged_existing_package['version'] !== $active_package['version']) { + if ($staged_existing_package->getVersion() !== $active_package->getVersion()) { $version_change_messages[] = $this->t( "@type '@name' from @active_version to @staged_version.", [ - '@type' => $type_map[$active_package['type']], - '@name' => $active_package['name'], - '@staged_version' => $staged_existing_package['version'], - '@active_version' => $active_package['version'], + '@type' => $type_map[$active_package->getType()], + '@name' => $active_package->getName(), + '@staged_version' => $staged_existing_package->getPrettyVersion(), + '@active_version' => $active_package->getPrettyVersion(), ] ); } diff --git a/src/Validator/UpdateVersionValidator.php b/src/Validator/UpdateVersionValidator.php index 5a4f7d3638..df3521bf9b 100644 --- a/src/Validator/UpdateVersionValidator.php +++ b/src/Validator/UpdateVersionValidator.php @@ -5,7 +5,6 @@ namespace Drupal\automatic_updates\Validator; use Drupal\automatic_updates\AutomaticUpdatesEvents; use Drupal\automatic_updates\Event\ReadinessCheckEvent; use Drupal\automatic_updates\Event\UpdateEvent; -use Drupal\automatic_updates\Updater; use Drupal\automatic_updates\Validation\ValidationResult; use Drupal\Core\Extension\ExtensionVersion; use Drupal\Core\StringTranslation\StringTranslationTrait; @@ -18,23 +17,6 @@ class UpdateVersionValidator implements EventSubscriberInterface { use StringTranslationTrait; - /** - * The updater service. - * - * @var \Drupal\automatic_updates\Updater - */ - protected $updater; - - /** - * Constructs an UpdateVersionSubscriber. - * - * @param \Drupal\automatic_updates\Updater $updater - * The updater service. - */ - public function __construct(Updater $updater) { - $this->updater = $updater; - } - /** * Returns the running core version, according to the Update module. * @@ -58,7 +40,7 @@ class UpdateVersionValidator implements EventSubscriberInterface { */ public function checkUpdateVersion(UpdateEvent $event): void { $from_version = ExtensionVersion::createFromVersionString($this->getCoreVersion()); - $core_package_names = $this->updater->getCorePackageNames(); + $core_package_names = $event->getActiveComposer()->getCorePackageNames(); // All the core packages will be updated to the same version, so it doesn't // matter which specific package we're looking at. $core_package_name = reset($core_package_names); diff --git a/tests/fixtures/fake-site/composer.json b/tests/fixtures/fake-site/composer.json new file mode 100644 index 0000000000..74d8204d88 --- /dev/null +++ b/tests/fixtures/fake-site/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "drupal/core": "*" + } +} diff --git a/tests/fixtures/fake-site/composer.lock b/tests/fixtures/fake-site/composer.lock new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/tests/fixtures/fake-site/composer.lock @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/project_staged_validation/new_project_added/active/composer.json b/tests/fixtures/project_staged_validation/new_project_added/active/composer.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/tests/fixtures/project_staged_validation/new_project_added/active/composer.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/project_staged_validation/new_project_added/staged/composer.json b/tests/fixtures/project_staged_validation/new_project_added/staged/composer.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/tests/fixtures/project_staged_validation/new_project_added/staged/composer.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/project_staged_validation/no_errors/active/composer.json b/tests/fixtures/project_staged_validation/no_errors/active/composer.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/tests/fixtures/project_staged_validation/no_errors/active/composer.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/project_staged_validation/no_errors/composer.json b/tests/fixtures/project_staged_validation/no_errors/composer.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/tests/fixtures/project_staged_validation/no_errors/composer.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/project_staged_validation/no_errors/staged/composer.json b/tests/fixtures/project_staged_validation/no_errors/staged/composer.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/tests/fixtures/project_staged_validation/no_errors/staged/composer.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/project_staged_validation/project_removed/active/composer.json b/tests/fixtures/project_staged_validation/project_removed/active/composer.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/tests/fixtures/project_staged_validation/project_removed/active/composer.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/project_staged_validation/project_removed/staged/composer.json b/tests/fixtures/project_staged_validation/project_removed/staged/composer.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/tests/fixtures/project_staged_validation/project_removed/staged/composer.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/project_staged_validation/version_changed/active/composer.json b/tests/fixtures/project_staged_validation/version_changed/active/composer.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/tests/fixtures/project_staged_validation/version_changed/active/composer.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/project_staged_validation/version_changed/staged/composer.json b/tests/fixtures/project_staged_validation/version_changed/staged/composer.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/tests/fixtures/project_staged_validation/version_changed/staged/composer.json @@ -0,0 +1 @@ +{} diff --git a/tests/src/Functional/ReadinessValidationTest.php b/tests/src/Functional/ReadinessValidationTest.php index 999067ce79..4db87cdf04 100644 --- a/tests/src/Functional/ReadinessValidationTest.php +++ b/tests/src/Functional/ReadinessValidationTest.php @@ -382,7 +382,7 @@ class ReadinessValidationTest extends AutomaticUpdatesFunctionalTestBase { $text = $this->getSession()->getPage()->find( 'css', - "h3#$section ~ details.system-status-report__entry:contains('Update readiness checks')", + "h3#$section ~ details.system-status-report__entry:contains('Update readiness checks')" )->getText(); $this->assertStringMatchesFormat($format, $text); } diff --git a/tests/src/Kernel/ReadinessValidation/DiskSpaceValidatorTest.php b/tests/src/Kernel/ReadinessValidation/DiskSpaceValidatorTest.php index 90ce7ab3a5..fa860f115c 100644 --- a/tests/src/Kernel/ReadinessValidation/DiskSpaceValidatorTest.php +++ b/tests/src/Kernel/ReadinessValidation/DiskSpaceValidatorTest.php @@ -208,7 +208,8 @@ class DiskSpaceValidatorTest extends KernelTestBase { $this->validator->sharedDisk = $shared_disk; $this->validator->freeSpace = array_map([Bytes::class, 'toNumber'], $free_space); - $event = new UpdateEvent(); + $composer = $this->createMock('\Drupal\package_manager\ComposerUtility'); + $event = new UpdateEvent($composer); $this->validator->checkDiskSpace($event); $this->assertValidationResultsEqual($expected_results, $event->getResults()); } diff --git a/tests/src/Kernel/ReadinessValidation/PendingUpdatesValidatorTest.php b/tests/src/Kernel/ReadinessValidation/PendingUpdatesValidatorTest.php index a304b7aad7..7254942eee 100644 --- a/tests/src/Kernel/ReadinessValidation/PendingUpdatesValidatorTest.php +++ b/tests/src/Kernel/ReadinessValidation/PendingUpdatesValidatorTest.php @@ -26,6 +26,13 @@ class PendingUpdatesValidatorTest extends KernelTestBase { 'update', ]; + /** + * The update event object that will be dispatched. + * + * @var \Drupal\automatic_updates\Event\UpdateEvent + */ + private $event; + /** * {@inheritdoc} */ @@ -42,16 +49,18 @@ class PendingUpdatesValidatorTest extends KernelTestBase { $this->container->get('keyvalue') ->get('post_update') ->set('existing_updates', $updates); + + $composer = $this->createMock('\Drupal\package_manager\ComposerUtility'); + $this->event = new UpdateEvent($composer); } /** * Tests that no error is raised if there are no pending updates. */ public function testNoPendingUpdates(): void { - $event = new UpdateEvent(); $this->container->get('automatic_updates.pending_updates_validator') - ->checkPendingUpdates($event); - $this->assertEmpty($event->getResults()); + ->checkPendingUpdates($this->event); + $this->assertEmpty($this->event->getResults()); } /** @@ -66,10 +75,9 @@ class PendingUpdatesValidatorTest extends KernelTestBase { $result = ValidationResult::createError(['Some modules have database schema updates to install. You should run the <a href="/update.php">database update script</a> immediately.']); - $event = new UpdateEvent(); $this->container->get('automatic_updates.pending_updates_validator') - ->checkPendingUpdates($event); - $this->assertValidationResultsEqual([$result], $event->getResults()); + ->checkPendingUpdates($this->event); + $this->assertValidationResultsEqual([$result], $this->event->getResults()); } /** @@ -80,10 +88,9 @@ class PendingUpdatesValidatorTest extends KernelTestBase { $result = ValidationResult::createError(['Some modules have database schema updates to install. You should run the <a href="/update.php">database update script</a> immediately.']); - $event = new UpdateEvent(); $this->container->get('automatic_updates.pending_updates_validator') - ->checkPendingUpdates($event); - $this->assertValidationResultsEqual([$result], $event->getResults()); + ->checkPendingUpdates($this->event); + $this->assertValidationResultsEqual([$result], $this->event->getResults()); } } diff --git a/tests/src/Traits/ValidationTestTrait.php b/tests/src/Traits/ValidationTestTrait.php index 7f313f23c9..70e97bc594 100644 --- a/tests/src/Traits/ValidationTestTrait.php +++ b/tests/src/Traits/ValidationTestTrait.php @@ -46,7 +46,7 @@ trait ValidationTestTrait { $this->testResults["checker_$listener_number"]['1 error 1 warning'] = [ "$listener_number:error" => ValidationResult::createError( [t("$listener_number:OMG 🔌. Some one unplugged the server! How is this site even running?")], - t("$listener_number:Summary: 🔥"), + t("$listener_number:Summary: 🔥") ), "$listener_number:warning" => ValidationResult::createWarning( [t("$listener_number:It looks like it going to rain and your server is outside.")], @@ -82,7 +82,7 @@ trait ValidationTestTrait { $this->testResults["checker_$listener_number"]['1 warning'] = [ ValidationResult::createWarning( [t("$listener_number:This is your one and only warning. You have been warned.")], - t("$listener_number:No need for this summary with only 1 warning."), + t("$listener_number:No need for this summary with only 1 warning.") ), ]; } diff --git a/tests/src/Unit/StagedProjectsValidatorTest.php b/tests/src/Unit/StagedProjectsValidatorTest.php index 6180fd9941..3c26777206 100644 --- a/tests/src/Unit/StagedProjectsValidatorTest.php +++ b/tests/src/Unit/StagedProjectsValidatorTest.php @@ -2,13 +2,12 @@ namespace Drupal\Tests\automatic_updates\Unit; -use Drupal\automatic_updates\Event\UpdateEvent; -use Drupal\automatic_updates\PathLocator; +use Drupal\automatic_updates\Event\PreCommitEvent; use Drupal\automatic_updates\Validator\StagedProjectsValidator; -use Drupal\Component\FileSystem\FileSystem; use Drupal\Core\StringTranslation\PluralTranslatableMarkup; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\StringTranslation\TranslationInterface; +use Drupal\package_manager\ComposerUtility; use Drupal\Tests\UnitTestCase; /** @@ -18,26 +17,42 @@ use Drupal\Tests\UnitTestCase; */ class StagedProjectsValidatorTest extends UnitTestCase { + /** + * Creates a pre-commit event object for testing. + * + * @param string $active_dir + * The active directory. + * @param string $stage_dir + * The stage directory. + * + * @return \Drupal\automatic_updates\Event\PreCommitEvent + * The event object. + */ + private function createEvent(string $active_dir, string $stage_dir): PreCommitEvent { + return new PreCommitEvent( + ComposerUtility::createForDirectory($active_dir), + ComposerUtility::createForDirectory($stage_dir) + ); + } + /** * Tests that if an exception is thrown, the update event will absorb it. */ public function testUpdateEventConsumesExceptionResults(): void { - $prefix = FileSystem::getOsTemporaryDirectory() . DIRECTORY_SEPARATOR; - $active_dir = uniqid($prefix); - $stage_dir = uniqid($prefix); + $message = 'An exception thrown by Composer at runtime.'; - $locator = $this->prophesize(PathLocator::class); - $locator->getActiveDirectory()->willReturn($active_dir); - $locator->getStageDirectory()->willReturn($stage_dir); - $validator = new StagedProjectsValidator(new TestTranslationManager(), $locator->reveal()); + $composer = $this->prophesize(ComposerUtility::class); + $composer->getDrupalExtensionPackages() + ->willThrow(new \RuntimeException($message)); + $event = new PreCommitEvent($composer->reveal(), $composer->reveal()); - $event = new UpdateEvent(); + $validator = new StagedProjectsValidator(new TestTranslationManager()); $validator->validateStagedProjects($event); $results = $event->getResults(); $this->assertCount(1, $results); $messages = reset($results)->getMessages(); $this->assertCount(1, $messages); - $this->assertSame("composer.lock file '$active_dir/composer.lock' not found.", (string) reset($messages)); + $this->assertSame($message, (string) reset($messages)); } /** @@ -56,14 +71,11 @@ class StagedProjectsValidatorTest extends UnitTestCase { * @covers ::validateStagedProjects */ public function testErrors(string $fixtures_dir, string $expected_summary, array $expected_messages): void { - $locator = $this->prophesize(PathLocator::class); $this->assertNotEmpty($fixtures_dir); $this->assertDirectoryExists($fixtures_dir); - $locator->getActiveDirectory()->willReturn("$fixtures_dir/active"); - $locator->getStageDirectory()->willReturn("$fixtures_dir/staged"); - $validator = new StagedProjectsValidator(new TestTranslationManager(), $locator->reveal()); - $event = new UpdateEvent(); + $event = $this->createEvent("$fixtures_dir/active", "$fixtures_dir/staged"); + $validator = new StagedProjectsValidator(new TestTranslationManager()); $validator->validateStagedProjects($event); $results = $event->getResults(); $this->assertCount(1, $results); @@ -121,11 +133,8 @@ class StagedProjectsValidatorTest extends UnitTestCase { */ public function testNoErrors(): void { $fixtures_dir = realpath(__DIR__ . '/../../fixtures/project_staged_validation/no_errors'); - $locator = $this->prophesize(PathLocator::class); - $locator->getActiveDirectory()->willReturn("$fixtures_dir/active"); - $locator->getStageDirectory()->willReturn("$fixtures_dir/staged"); - $validator = new StagedProjectsValidator(new TestTranslationManager(), $locator->reveal()); - $event = new UpdateEvent(); + $event = $this->createEvent("$fixtures_dir/active", "$fixtures_dir/staged"); + $validator = new StagedProjectsValidator(new TestTranslationManager()); $validator->validateStagedProjects($event); $results = $event->getResults(); $this->assertIsArray($results); @@ -137,19 +146,14 @@ class StagedProjectsValidatorTest extends UnitTestCase { */ public function testNoLockFile(): void { $fixtures_dir = realpath(__DIR__ . '/../../fixtures/project_staged_validation/no_errors'); - $locator = $this->prophesize(PathLocator::class); - $locator->getActiveDirectory()->willReturn("$fixtures_dir/active"); - $locator->getStageDirectory()->willReturn("$fixtures_dir"); - $validator = new StagedProjectsValidator(new TestTranslationManager(), $locator->reveal()); - $event = new UpdateEvent(); + + $event = $this->createEvent("$fixtures_dir/active", $fixtures_dir); + $validator = new StagedProjectsValidator(new TestTranslationManager()); $validator->validateStagedProjects($event); $results = $event->getResults(); $this->assertCount(1, $results); $result = array_pop($results); - $this->assertMatchesRegularExpression( - "/.*automatic_updates\/tests\/fixtures\/project_staged_validation\/no_errors\/composer.lock' not found/", - (string) $result->getMessages()[0] - ); + $this->assertSame("No lockfile found. Unable to read locked packages", (string) $result->getMessages()[0]); $this->assertSame('', (string) $result->getSummary()); } -- GitLab