diff --git a/automatic_updates_extensions/automatic_updates_extensions.services.yml b/automatic_updates_extensions/automatic_updates_extensions.services.yml index a139a889b62aceb910b0e913c63c20869a1adca5..acac491c30d1580bf01acea0732407717f8331ea 100644 --- a/automatic_updates_extensions/automatic_updates_extensions.services.yml +++ b/automatic_updates_extensions/automatic_updates_extensions.services.yml @@ -9,3 +9,6 @@ services: Drupal\automatic_updates_extensions\Validator\ForbidCoreChangesValidator: tags: - { name: event_subscriber } + Drupal\automatic_updates_extensions\Validator\RequestedUpdateValidator: + tags: + - { name: event_subscriber } diff --git a/automatic_updates_extensions/src/Validator/RequestedUpdateValidator.php b/automatic_updates_extensions/src/Validator/RequestedUpdateValidator.php new file mode 100644 index 0000000000000000000000000000000000000000..1d0a1c0eb7b181dd7d1feb9ee8e0ef49db8f94be --- /dev/null +++ b/automatic_updates_extensions/src/Validator/RequestedUpdateValidator.php @@ -0,0 +1,105 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\automatic_updates_extensions\Validator; + +use Composer\Semver\Semver; +use Drupal\automatic_updates_extensions\ExtensionUpdateStage; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\package_manager\ComposerInspector; +use Drupal\package_manager\Event\PreApplyEvent; +use Drupal\package_manager\Event\StatusCheckEvent; +use Drupal\package_manager\PathLocator; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * Validates that requested packages have been updated. + * + * @internal + * This is an internal part of Automatic Updates and may be changed or removed + * at any time without warning. External code should not interact with this + * class. + */ +final class RequestedUpdateValidator implements EventSubscriberInterface { + + use StringTranslationTrait; + + /** + * Constructs a RequestedUpdateValidator object. + * + * @param \Drupal\package_manager\ComposerInspector $composerInspector + * The Composer inspector service. + * @param \Drupal\package_manager\PathLocator $pathLocator + * The path locator service. + */ + public function __construct( + private readonly ComposerInspector $composerInspector, + private readonly PathLocator $pathLocator, + ) {} + + /** + * Validates that requested packages have been updated to the right version. + * + * @param \Drupal\package_manager\Event\PreApplyEvent|\Drupal\package_manager\Event\StatusCheckEvent $event + * The pre-apply event. + */ + public function checkRequestedStagedVersion(PreApplyEvent|StatusCheckEvent $event): void { + $stage = $event->stage; + if ($stage->getType() !== 'automatic_updates_extensions:attended' || !$stage->stageDirectoryExists()) { + return; + } + $requested_package_versions = $stage->getPackageVersions(); + $active = $this->composerInspector->getInstalledPackagesList($this->pathLocator->getProjectRoot()); + $staged = $this->composerInspector->getInstalledPackagesList($event->stage->getStageDirectory()); + $changed_stage_packages = $staged->getPackagesWithDifferentVersionsIn($active)->getArrayCopy(); + + if (empty($changed_stage_packages)) { + $event->addError([$this->t('No updates detected in the staging area.')]); + return; + } + + // Check for all changed the packages if they are updated to the requested + // version. + foreach (['production', 'dev'] as $package_type) { + foreach ($requested_package_versions[$package_type] as $requested_package_name => $requested_version) { + if (array_key_exists($requested_package_name, $changed_stage_packages)) { + $staged_version = $changed_stage_packages[$requested_package_name]->version; + if (!Semver::satisfies($staged_version, $requested_version)) { + $event->addError([ + $this->t( + "The requested update to '@requested_package_name' to version '@requested_version' does not match the actual staged update to '@staged_version'.", + [ + '@requested_package_name' => $requested_package_name, + '@requested_version' => $requested_version, + '@staged_version' => $staged_version, + ] + ), + ]); + } + } + else { + $event->addError([ + $this->t( + "The requested update to '@requested_package_name' to version '@requested_version' was not performed.", + [ + '@requested_package_name' => $requested_package_name, + '@requested_version' => $requested_version, + ] + ), + ]); + } + } + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents(): array { + $events[StatusCheckEvent::class][] = ['checkRequestedStagedVersion']; + $events[PreApplyEvent::class][] = ['checkRequestedStagedVersion']; + return $events; + } + +} diff --git a/automatic_updates_extensions/tests/src/Functional/StatusCheckerRunAfterUpdateTest.php b/automatic_updates_extensions/tests/src/Functional/StatusCheckerRunAfterUpdateTest.php index cd782c22e1f53a147357f378248bb990ba80133d..35c6d023f3adb159fe383e1d8941912f99ab4d6d 100644 --- a/automatic_updates_extensions/tests/src/Functional/StatusCheckerRunAfterUpdateTest.php +++ b/automatic_updates_extensions/tests/src/Functional/StatusCheckerRunAfterUpdateTest.php @@ -48,6 +48,7 @@ class StatusCheckerRunAfterUpdateTest extends UpdaterFormTestBase { $assert_session->pageTextNotContains(static::$warningsExplanation); $this->assertTableShowsUpdates('Semver Test', '8.1.0', '8.1.1'); + $this->getStageFixtureManipulator()->setVersion('drupal/semver_test', '8.1.1'); $this->assertUpdatesCount(1); $page->checkField('projects[semver_test]'); $page->pressButton('Update'); diff --git a/automatic_updates_extensions/tests/src/Functional/SuccessfulUpdateTest.php b/automatic_updates_extensions/tests/src/Functional/SuccessfulUpdateTest.php index fe0b85e685b49c070d7406ba32639956332b7f61..5b2e3f5a27889d79bc407e635f06f3f72f62bf5a 100644 --- a/automatic_updates_extensions/tests/src/Functional/SuccessfulUpdateTest.php +++ b/automatic_updates_extensions/tests/src/Functional/SuccessfulUpdateTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Drupal\Tests\automatic_updates_extensions\Functional; use Drupal\package_manager\Event\PreApplyEvent; +use Drupal\package_manager\LegacyVersionUtility; use Drupal\package_manager_test_validation\StagedDatabaseUpdateValidator; /** @@ -60,6 +61,7 @@ class SuccessfulUpdateTest extends UpdaterFormTestBase { $path_to_fixtures_folder = $project_name === 'aaa_update_test' ? '/../../../../package_manager/tests' : '/../..'; $this->setReleaseMetadata(__DIR__ . $path_to_fixtures_folder . '/fixtures/release-history/' . $project_name . '.1.1.xml'); $this->setProjectInstalledVersion([$project_name => $installed_version]); + $this->getStageFixtureManipulator()->setVersion('drupal/' . $project_name, LegacyVersionUtility::convertToSemanticVersion($target_version)); $this->checkForUpdates(); $state = $this->container->get('state'); $state->set('system.maintenance_mode', $maintenance_mode_on); diff --git a/automatic_updates_extensions/tests/src/Functional/UnsuccessfulUpdateTest.php b/automatic_updates_extensions/tests/src/Functional/UnsuccessfulUpdateTest.php index 3da2f77655797347d2c3e8b6b6fd49bca57be88b..673c9561fc3fce1bd2b7099e81684289a296d003 100644 --- a/automatic_updates_extensions/tests/src/Functional/UnsuccessfulUpdateTest.php +++ b/automatic_updates_extensions/tests/src/Functional/UnsuccessfulUpdateTest.php @@ -31,6 +31,7 @@ class UnsuccessfulUpdateTest extends UpdaterFormTestBase { $this->drupalGet('/admin/reports/updates'); $this->clickLink('Update Extensions'); $this->assertTableShowsUpdates('Semver Test', '8.1.0', '8.1.1'); + $this->getStageFixtureManipulator()->setVersion('drupal/semver_test', '8.1.1'); $this->assertUpdatesCount(1); $this->checkForMetaRefresh(); $assert->pageTextNotContains(static::$errorsExplanation); diff --git a/automatic_updates_extensions/tests/src/Functional/UpdateErrorTest.php b/automatic_updates_extensions/tests/src/Functional/UpdateErrorTest.php index b90894280731c2773cf5dc60f9c703732cb6465b..8acb822a15822d55337bd2a9beab9739cfc90f7a 100644 --- a/automatic_updates_extensions/tests/src/Functional/UpdateErrorTest.php +++ b/automatic_updates_extensions/tests/src/Functional/UpdateErrorTest.php @@ -32,6 +32,7 @@ class UpdateErrorTest extends UpdaterFormTestBase { $assert_session->pageTextNotContains(static::$warningsExplanation); $this->assertTableShowsUpdates('Semver Test', '8.1.0', '8.1.1'); + $this->getStageFixtureManipulator()->setVersion('drupal/semver_test', '8.1.1'); $this->assertUpdatesCount(1); $page->checkField('projects[semver_test]'); $page->pressButton('Update'); diff --git a/automatic_updates_extensions/tests/src/Kernel/AutomaticUpdatesExtensionsKernelTestBase.php b/automatic_updates_extensions/tests/src/Kernel/AutomaticUpdatesExtensionsKernelTestBase.php index 597b6e9b66809d908b1638f11428eda7ae0d3f3d..1e022e6aa92de100cb320d7bfbb22aa2b60b46fd 100644 --- a/automatic_updates_extensions/tests/src/Kernel/AutomaticUpdatesExtensionsKernelTestBase.php +++ b/automatic_updates_extensions/tests/src/Kernel/AutomaticUpdatesExtensionsKernelTestBase.php @@ -56,7 +56,7 @@ abstract class AutomaticUpdatesExtensionsKernelTestBase extends AutomaticUpdates ], TRUE) ->addPackage([ "name" => "drupal/semver_test", - "version" => "1.0.0", + "version" => "8.1.0", "type" => "drupal-module", ]) ->addPackage([ diff --git a/automatic_updates_extensions/tests/src/Kernel/ExtensionUpdateStageTest.php b/automatic_updates_extensions/tests/src/Kernel/ExtensionUpdateStageTest.php index 76e8e06daf83e86ae8a5be9fe38f15f031748b3b..505abe68589c360f42457c6260ee5347e6b51dd9 100644 --- a/automatic_updates_extensions/tests/src/Kernel/ExtensionUpdateStageTest.php +++ b/automatic_updates_extensions/tests/src/Kernel/ExtensionUpdateStageTest.php @@ -100,6 +100,7 @@ class ExtensionUpdateStageTest extends AutomaticUpdatesExtensionsKernelTestBase [ 'update', '--with-all-dependencies', + '--optimize-autoloader', 'drupal/my_module:9.8.1', 'drupal/my_dev_module:1.2.0-alpha1@alpha', ], diff --git a/automatic_updates_extensions/tests/src/Kernel/Validator/RequestedUpdateValidatorTest.php b/automatic_updates_extensions/tests/src/Kernel/Validator/RequestedUpdateValidatorTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1e2d33ae6caaf1437e586e46539e9f3b8198a1b3 --- /dev/null +++ b/automatic_updates_extensions/tests/src/Kernel/Validator/RequestedUpdateValidatorTest.php @@ -0,0 +1,114 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\automatic_updates_extensions\Kernel\Validator; + +use Drupal\automatic_updates_extensions\ExtensionUpdateStage; +use Drupal\fixture_manipulator\ActiveFixtureManipulator; +use Drupal\package_manager\Exception\StageEventException; +use Drupal\package_manager\ValidationResult; +use Drupal\Tests\automatic_updates_extensions\Kernel\AutomaticUpdatesExtensionsKernelTestBase; + +/** + * @coversDefaultClass \Drupal\automatic_updates_extensions\Validator\RequestedUpdateValidator + * @group automatic_updates_extensions + * @internal + */ +class RequestedUpdateValidatorTest extends AutomaticUpdatesExtensionsKernelTestBase { + + /** + * Tests error messages if requested updates were not staged. + * + * @param array $staged_versions + * An array of the staged versions where the keys are the package names and + * the values are the package versions. + * @param array $expected_results + * The expected validation results. + * + * @dataProvider providerTestErrorMessage + */ + public function testErrorMessage(array $staged_versions, array $expected_results): void { + if ($staged_versions) { + // If we are going to stage updates to Drupal packages also update a + // non-Drupal. The validator should ignore the non-Drupal packages. + (new ActiveFixtureManipulator()) + ->addPackage([ + "name" => 'vendor/non-drupal-package', + "version" => "1.0.0", + "type" => "drupal-module", + ]) + ->commitChanges(); + $this->getStageFixtureManipulator()->setVersion('vendor/non-drupal-package', '1.0.1'); + foreach ($staged_versions as $package => $version) { + $this->getStageFixtureManipulator()->setVersion($package, $version); + } + } + + $this->setReleaseMetadata([ + 'semver_test' => __DIR__ . '/../../../fixtures/release-history/semver_test.1.1.xml', + 'drupal' => __DIR__ . '/../../../../../package_manager/tests/fixtures/release-history/drupal.9.8.2.xml', + 'aaa_update_test' => __DIR__ . "/../../../../../package_manager/tests/fixtures/release-history/aaa_update_test.1.1.xml", + ]); + // Set the project version to '8.0.1' so that there 2 versions of above this + // that will be in the list of supported releases, 8.1.0 and 8.1.1. + (new ActiveFixtureManipulator()) + ->setVersion('drupal/semver_test', '8.0.1') + ->commitChanges(); + // @todo Replace with use of the trait from the Update module in https://drupal.org/i/3348234. + $module_info = ['version' => '8.0.1', 'project' => 'semver_test']; + $this->config('update_test.settings') + ->set("system_info.semver_test", $module_info) + ->save(); + + $stage = $this->container->get(ExtensionUpdateStage::class); + $stage->begin([ + 'semver_test' => '8.1.1', + 'aaa_update_test' => '8.x-1.1', + ]); + $stage->stage(); + $this->assertStatusCheckResults($expected_results, $stage); + try { + $stage->apply(); + $this->fail('Expecting an exception.'); + } + catch (StageEventException $exception) { + $this->assertExpectedResultsFromException($expected_results, $exception); + } + } + + /** + * Data provider for testErrorMessage(). + * + * @return mixed[] + * The test cases. + */ + public function providerTestErrorMessage() { + return [ + 'no updates' => [ + [], + [ + ValidationResult::createError([t('No updates detected in the staging area.')]), + ], + ], + '1 project not updated' => [ + [ + 'drupal/aaa_update_test' => '1.1.0', + ], + [ + ValidationResult::createError([t("The requested update to 'drupal/semver_test' to version '8.1.1' was not performed.")]), + ], + ], + 'project updated to wrong version' => [ + [ + 'drupal/semver_test' => '8.1.0', + 'drupal/aaa_update_test' => '1.1.0', + ], + [ + ValidationResult::createError([t("The requested update to 'drupal/semver_test' to version '8.1.1' does not match the actual staged update to '8.1.0'.")]), + ], + ], + ]; + } + +} diff --git a/automatic_updates_extensions/tests/src/Kernel/Validator/UpdateReleaseValidatorTest.php b/automatic_updates_extensions/tests/src/Kernel/Validator/UpdateReleaseValidatorTest.php index 4ff92ffd7c6f1d6a9543f8fda25e4a7dc0439bdd..c5ad35509855db93291903697664d3f78343fb1a 100644 --- a/automatic_updates_extensions/tests/src/Kernel/Validator/UpdateReleaseValidatorTest.php +++ b/automatic_updates_extensions/tests/src/Kernel/Validator/UpdateReleaseValidatorTest.php @@ -79,6 +79,9 @@ class UpdateReleaseValidatorTest extends AutomaticUpdatesExtensionsKernelTestBas ]; } else { + // Ensure the correct version of the package is staged because the update + // is expected to succeed. + $this->getStageFixtureManipulator()->setVersion("drupal/$project", LegacyVersionUtility::convertToSemanticVersion($target_version)); $expected_results = []; } diff --git a/package_manager/src/StageBase.php b/package_manager/src/StageBase.php index c51efdfef6c8880070f827eddcc4f97fc02ac771..4416133dd19615e7f1d05318d24336b8b8a8ceed 100644 --- a/package_manager/src/StageBase.php +++ b/package_manager/src/StageBase.php @@ -456,7 +456,7 @@ abstract class StageBase implements LoggerAwareInterface { // If constraints were changed, update those packages. if ($runtime || $dev) { - $command = array_merge(['update', '--with-all-dependencies'], $runtime, $dev); + $command = array_merge(['update', '--with-all-dependencies', '--optimize-autoloader'], $runtime, $dev); $do_stage($command); } $this->dispatch(new PostRequireEvent($this, $runtime, $dev)); diff --git a/tests/src/Kernel/UpdateStageTest.php b/tests/src/Kernel/UpdateStageTest.php index 0169b8d2e916541ea931e3b94f1e589516fc1ae8..bc1d323a65549765e1d44f4bef8d6f9e21f9b474 100644 --- a/tests/src/Kernel/UpdateStageTest.php +++ b/tests/src/Kernel/UpdateStageTest.php @@ -102,6 +102,7 @@ class UpdateStageTest extends AutomaticUpdatesKernelTestBase { [ 'update', '--with-all-dependencies', + '--optimize-autoloader', 'drupal/core-recommended:9.8.1', 'drupal/core-dev:9.8.1', ],