Issue #3108658 by alexpott, mikelutz, catch, xjm, nicxvan, longwave: Handling update path divergence between 11.x and 10.x
parent
0426157519
commit
df700a8d66
|
@ -11,6 +11,7 @@
|
|||
use Drupal\Component\Graph\Graph;
|
||||
use Drupal\Core\Extension\Exception\UnknownExtensionException;
|
||||
use Drupal\Core\Utility\Error;
|
||||
use Drupal\Core\Update\EquivalentUpdate;
|
||||
|
||||
/**
|
||||
* Returns whether the minimum schema requirement has been satisfied.
|
||||
|
@ -166,7 +167,13 @@ function update_do_one($module, $number, $dependency_map, &$context) {
|
|||
}
|
||||
|
||||
$ret = [];
|
||||
if (function_exists($function)) {
|
||||
$equivalent_update = \Drupal::service('update.update_hook_registry')->getEquivalentUpdate($module, $number);
|
||||
if ($equivalent_update instanceof EquivalentUpdate) {
|
||||
$ret['results']['query'] = $equivalent_update->toSkipMessage();
|
||||
$ret['results']['success'] = TRUE;
|
||||
$context['sandbox']['#finished'] = TRUE;
|
||||
}
|
||||
elseif (function_exists($function)) {
|
||||
try {
|
||||
$ret['results']['query'] = $function($context['sandbox']);
|
||||
$ret['results']['success'] = TRUE;
|
||||
|
|
|
@ -667,6 +667,82 @@ function hook_install_tasks_alter(&$tasks, $install_state) {
|
|||
* See the @link batch Batch operations topic @endlink for more information on
|
||||
* how to use the Batch API.
|
||||
*
|
||||
* @section sec_equivalent_updates Multiple upgrade paths
|
||||
* There are situations where changes require a hook_update_N() to be applied to
|
||||
* different major branches. This results in more than one upgrade path from the
|
||||
* current major version to the next major version.
|
||||
*
|
||||
* For example, if an update is added to 11.1.0 and 10.4.0, then a site on
|
||||
* 10.3.7 can update either to 10.4.0 and from there to 11.1.0, or directly from
|
||||
* 10.3.7 to 11.1.1. In one case, the update will run on the 10.x code base, and
|
||||
* in another on the 11.x code base, but the update system needs to ensure that
|
||||
* it doesn't run twice on the same site.
|
||||
*
|
||||
* hook_update_N() numbers are sequential integers, and numbers lower than the
|
||||
* modules current schema version will never be run. This means once a site has
|
||||
* run an update, for example, 11100, it will not run a later update added as
|
||||
* 10400. Backporting of updates therefore needs to allow 'space' for the 10.4.x
|
||||
* codebase to include updates which don't exist in 11.x (for example to ensure
|
||||
* a later 11.x update goes smoothly).
|
||||
*
|
||||
* To resolve this, when handling potential backports of updates between major
|
||||
* branches, we use different update numbers for each branch, but record the
|
||||
* relationship between those updates in the older branches. This is best
|
||||
* explained by an example showing the different branches updates could be
|
||||
* applied to:
|
||||
* - The first update, system_update_10300 is applied to 10.3.0.
|
||||
* - Then, 11.0.0 is released with the update removed,
|
||||
* system_update_last_removed() is added which returns 10300.
|
||||
* - The next update, system_update_11100, is applied to 11.1.x only.
|
||||
* - Then 10.4.0 and 11.1.0 are released. system_update_11100 is not backported
|
||||
* to 11.0.x or any 10.x branch.
|
||||
* - Finally, a critical data loss update is necessary. The bug-fix supported
|
||||
* branches are 11.1.x, 11.0.x, and 10.4.x. This results in adding the updates
|
||||
* system_update_10400 (10.4.x), system_update_11000 (11.0.x) and
|
||||
* system_update_11101 (11.1.x) and making the 10.4.1, 11.0.1 and 11.1.1
|
||||
* releases.
|
||||
*
|
||||
* This is a list of the example releases and the updates they contain:
|
||||
* - 10.3.0: system_update_10300
|
||||
* - 10.4.1: system_update_10300 and system_update_10400 (equivalent to
|
||||
* system_update_11101)
|
||||
* - 11.0.0: No updates
|
||||
* - 11.0.1: system_update_11000 (equivalent to system_update_11101)
|
||||
* - 11.1.0: system_update_11100
|
||||
* - 11.1.1: system_update_11100 and system_update_11101
|
||||
*
|
||||
* In this situation, sites on 10.4.1 or 11.0.1 will be required to update to
|
||||
* versions that contain system_update_11101. For example, a site on 10.4.1
|
||||
* would not be able to update to 11.0.0, because that would result in it going
|
||||
* 'backwards' in terms of database schema, but it would be able to update to
|
||||
* 11.1.1. The same is true for a site on 11.0.1.
|
||||
*
|
||||
* The following examples show how to implement a hook_update_N() that must be
|
||||
* skipped in a future update process.
|
||||
*
|
||||
* Future updates can be marked as equivalent by adding the following code to an
|
||||
* update.
|
||||
* @code
|
||||
* function my_module_update_10400() {
|
||||
* \Drupal::service('update.update_hook_registry')->markFutureUpdateEquivalent(11101, '11.1.1');
|
||||
*
|
||||
* // The rest of the update function.
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* At the moment we need to add defensive coding in the future update to ensure
|
||||
* it is skipped.
|
||||
* @code
|
||||
* function my_module_update_11101() {
|
||||
* $equivalent_update = \Drupal::service('update.update_hook_registry')->getEquivalentUpdate();
|
||||
* if ($equivalent_update instanceof \Drupal\Core\Update\EquivalentUpdate) {
|
||||
* return $equivalent_update->toSkipMessage();
|
||||
* }
|
||||
*
|
||||
* // The rest of the update function.
|
||||
* }
|
||||
* @encode
|
||||
*
|
||||
* @param array $sandbox
|
||||
* Stores information for batch updates. See above for more information.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Core\Update;
|
||||
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
|
||||
/**
|
||||
* Value object to hold information about an equivalent update.
|
||||
*
|
||||
* @see module.api.php
|
||||
*/
|
||||
final class EquivalentUpdate {
|
||||
|
||||
/**
|
||||
* Constructs a EquivalentUpdate object.
|
||||
*
|
||||
* @param string $module
|
||||
* The module the update is for.
|
||||
* @param int $future_update
|
||||
* The equivalent future update.
|
||||
* @param int $ran_update
|
||||
* The update that already ran and registered the equivalent update.
|
||||
* @param string $future_version
|
||||
* The future version that has the expected update.
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $module,
|
||||
public readonly int $future_update,
|
||||
public readonly int $ran_update,
|
||||
public readonly string $future_version,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a message to explain why an update has been skipped.
|
||||
*
|
||||
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
|
||||
* An message explaining why an update has been skipped.
|
||||
*/
|
||||
public function toSkipMessage(): TranslatableMarkup {
|
||||
return new TranslatableMarkup(
|
||||
'Update @number for the @module module has been skipped because the equivalent change was already made in update @ran_update.',
|
||||
['@number' => $this->future_update, '@module' => $this->module, '@ran_update' => $this->ran_update]
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
namespace Drupal\Core\Update;
|
||||
|
||||
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
|
||||
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
|
||||
|
||||
/**
|
||||
* Provides module updates versions handling.
|
||||
|
@ -14,6 +15,11 @@ class UpdateHookRegistry {
|
|||
*/
|
||||
public const SCHEMA_UNINSTALLED = -1;
|
||||
|
||||
/**
|
||||
* Regular expression to match all possible defined hook_update_N().
|
||||
*/
|
||||
private const FUNC_NAME_REGEXP = '/^(?<module>.+)_update_(?<version>\d+)$/';
|
||||
|
||||
/**
|
||||
* A list of enabled modules.
|
||||
*
|
||||
|
@ -22,12 +28,28 @@ class UpdateHookRegistry {
|
|||
protected $enabledModules;
|
||||
|
||||
/**
|
||||
* The key value storage.
|
||||
* The system.schema key value storage.
|
||||
*
|
||||
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
|
||||
*/
|
||||
protected $keyValue;
|
||||
|
||||
/**
|
||||
* The core.equivalent_updates key value storage.
|
||||
*
|
||||
* The key value keys are modules and the value is an array of equivalent
|
||||
* updates with the following shape:
|
||||
* - The array keys are the equivalent future update numbers.
|
||||
* - The value is an array containing two keys:
|
||||
* - 'ran_update': The update that registered the future update as an
|
||||
* equivalent.
|
||||
* - 'future_version_string': The version that provides the future update.
|
||||
*
|
||||
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
|
||||
* @see module.api.php
|
||||
*/
|
||||
protected KeyValueStoreInterface $equivalentUpdates;
|
||||
|
||||
/**
|
||||
* A static cache of schema currentVersions per module.
|
||||
*
|
||||
|
@ -63,6 +85,7 @@ class UpdateHookRegistry {
|
|||
) {
|
||||
$this->enabledModules = array_keys($module_list);
|
||||
$this->keyValue = $key_value_factory->get('system.schema');
|
||||
$this->equivalentUpdates = $key_value_factory->get('core.equivalent_updates');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -83,9 +106,6 @@ class UpdateHookRegistry {
|
|||
$this->allAvailableSchemaVersions[$enabled_module] = [];
|
||||
}
|
||||
|
||||
// Prepare regular expression to match all possible defined
|
||||
// hook_update_N().
|
||||
$regexp = '/^(?<module>.+)_update_(?<version>\d+)$/';
|
||||
$functions = get_defined_functions();
|
||||
// Narrow this down to functions ending with an integer, since all
|
||||
// hook_update_N() functions end this way, and there are other
|
||||
|
@ -96,7 +116,7 @@ class UpdateHookRegistry {
|
|||
foreach (preg_grep('/_\d+$/', $functions['user']) as $function) {
|
||||
// If this function is a module update function, add it to the list of
|
||||
// module updates.
|
||||
if (preg_match($regexp, $function, $matches)) {
|
||||
if (preg_match(self::FUNC_NAME_REGEXP, $function, $matches)) {
|
||||
$this->allAvailableSchemaVersions[$matches['module']][] = (int) $matches['version'];
|
||||
}
|
||||
}
|
||||
|
@ -139,6 +159,7 @@ class UpdateHookRegistry {
|
|||
*/
|
||||
public function setInstalledVersion(string $module, int $version): self {
|
||||
$this->keyValue->set($module, $version);
|
||||
$this->deleteEquivalentUpdate($module, $version);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -150,6 +171,7 @@ class UpdateHookRegistry {
|
|||
*/
|
||||
public function deleteInstalledVersion(string $module): void {
|
||||
$this->keyValue->delete($module);
|
||||
$this->equivalentUpdates->delete($module);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -164,4 +186,131 @@ class UpdateHookRegistry {
|
|||
return $this->keyValue->getAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a future update as equivalent to the current update running.
|
||||
*
|
||||
* Updates can be marked as equivalent when they are backported to a
|
||||
* previous, but still supported, major version. For example:
|
||||
* - A 2.x hook_update_N() would be added as normal, for example:
|
||||
* MODULE_update_2005().
|
||||
* - When that same update is backported to 1.x, it is given its own update
|
||||
* number, for example: MODULE_update_1040(). In this update, a call to
|
||||
* @code
|
||||
* \Drupal::service('update.update_hook_registry')->markFutureUpdateEquivalent(2005, '2.10')
|
||||
* @endcode
|
||||
* is added to ensure that a site that has run this update does not run
|
||||
* MODULE_update_2005().
|
||||
*
|
||||
* @param int $future_update_number
|
||||
* The future update number.
|
||||
* @param string $future_version_string
|
||||
* The version that contains the future update.
|
||||
*/
|
||||
public function markFutureUpdateEquivalent(int $future_update_number, string $future_version_string): void {
|
||||
[$module, $ran_update_number] = $this->determineModuleAndVersion();
|
||||
|
||||
if ($ran_update_number > $future_update_number) {
|
||||
throw new \LogicException(sprintf(
|
||||
'Cannot mark the update %d as an equivalent since it is less than the current update %d for the %s module ',
|
||||
$future_update_number, $ran_update_number, $module
|
||||
));
|
||||
}
|
||||
|
||||
$data = $this->equivalentUpdates->get($module, []);
|
||||
// It does not matter if $data[$future_update_number] is already set. If two
|
||||
// updates are causing the same update to be marked as equivalent then the
|
||||
// latest information is the correct information to use.
|
||||
$data[$future_update_number] = [
|
||||
'ran_update' => $ran_update_number,
|
||||
'future_version_string' => $future_version_string,
|
||||
];
|
||||
$this->equivalentUpdates->set($module, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the EquivalentUpdate object for an update.
|
||||
*
|
||||
* @param string|null $module
|
||||
* The module providing the update. If this is NULL the update to check will
|
||||
* be determined from the backtrace.
|
||||
* @param int|null $version
|
||||
* The update to check. If this is NULL the update to check will
|
||||
* be determined from the backtrace.
|
||||
*
|
||||
* @return \Drupal\Core\Update\EquivalentUpdate|null
|
||||
* A value object with the equivalent update information or NULL if the
|
||||
* update does not have an equivalent update.
|
||||
*/
|
||||
public function getEquivalentUpdate(?string $module = NULL, ?int $version = NULL): ?EquivalentUpdate {
|
||||
if ($module === NULL || $version === NULL) {
|
||||
[$module, $version] = $this->determineModuleAndVersion();
|
||||
}
|
||||
$data = $this->equivalentUpdates->get($module, []);
|
||||
if (isset($data[$version]['ran_update'])) {
|
||||
return new EquivalentUpdate(
|
||||
$module,
|
||||
$version,
|
||||
$data[$version]['ran_update'],
|
||||
$data[$version]['future_version_string'],
|
||||
);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the module and update number from the stack trace.
|
||||
*
|
||||
* @return array<string, int>
|
||||
* An array with two values. The first value is the module name and the
|
||||
* second value is the update number.
|
||||
*/
|
||||
private function determineModuleAndVersion(): array {
|
||||
$stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
|
||||
|
||||
for ($i = 0; $i < count($stack); $i++) {
|
||||
if (preg_match(self::FUNC_NAME_REGEXP, $stack[$i]['function'], $matches)) {
|
||||
return [$matches['module'], $matches['version']];
|
||||
}
|
||||
}
|
||||
|
||||
throw new \BadMethodCallException(__METHOD__ . ' must be called from a hook_update_N() function');
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an equivalent update.
|
||||
*
|
||||
* @param string $module
|
||||
* The module providing the update.
|
||||
* @param int $version
|
||||
* The equivalent update to remove.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if an equivalent update was removed, or FALSE if it was not.
|
||||
*/
|
||||
protected function deleteEquivalentUpdate(string $module, int $version): bool {
|
||||
$data = $this->equivalentUpdates->get($module, []);
|
||||
if (isset($data[$version])) {
|
||||
unset($data[$version]);
|
||||
if (empty($data)) {
|
||||
$this->equivalentUpdates->delete($module);
|
||||
}
|
||||
else {
|
||||
$this->equivalentUpdates->set($module, $data);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the equivalent update information for all modules.
|
||||
*
|
||||
* @return array<string, array<int, array{ran_update:int, future_version_string: string}>>
|
||||
* Array of modules as the keys and values as arrays of equivalent update
|
||||
* information.
|
||||
*/
|
||||
public function getAllEquivalentUpdates(): array {
|
||||
return $this->equivalentUpdates->getAll();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -41,6 +41,9 @@ autoreply
|
|||
autosubmit
|
||||
backlink
|
||||
backlinks
|
||||
backported
|
||||
backporting
|
||||
backports
|
||||
bakeware
|
||||
barbar
|
||||
barchart
|
||||
|
|
|
@ -1511,6 +1511,7 @@ function system_requirements($phase) {
|
|||
}
|
||||
// Also check post-updates. Only do this if we're not already showing an
|
||||
// error for hook_update_N().
|
||||
$missing_updates = [];
|
||||
if (empty($module_list)) {
|
||||
$existing_updates = \Drupal::service('keyvalue')->get('post_update')->get('existing_updates', []);
|
||||
$post_update_registry = \Drupal::service('update.post_update_registry');
|
||||
|
@ -1537,6 +1538,42 @@ function system_requirements($phase) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($missing_updates)) {
|
||||
foreach ($update_registry->getAllEquivalentUpdates() as $module => $equivalent_updates) {
|
||||
$module_info = $module_extension_list->get($module);
|
||||
foreach ($equivalent_updates as $future_update => $data) {
|
||||
$future_update_function_name = $module . '_update_' . $future_update;
|
||||
$ran_update_function_name = $module . '_update_' . $data['ran_update'];
|
||||
// If an update was marked as an equivalent by a previous update, and
|
||||
// both the previous update and the equivalent update are not found in
|
||||
// the current code base, prevent updating. This indicates a site
|
||||
// attempting to go 'backwards' in terms of database schema.
|
||||
// @see \Drupal\Core\Update\UpdateHookRegistry::markFutureUpdateEquivalent()
|
||||
if (!function_exists($ran_update_function_name) && !function_exists($future_update_function_name)) {
|
||||
// If the module is provided by core prepend helpful text as the
|
||||
// module does not exist in composer or Drupal.org.
|
||||
if (str_starts_with($module_info->getPathname(), 'core/')) {
|
||||
$future_version_string = 'Drupal Core ' . $data['future_version_string'];
|
||||
}
|
||||
else {
|
||||
$future_version_string = $data['future_version_string'];
|
||||
}
|
||||
$requirements[$module . '_equivalent_update_missing'] = [
|
||||
'title' => t('Missing updates for: @module', ['@module' => $module_info->info['name']]),
|
||||
'description' => t('The version of the %module module that you are attempting to update to is missing update @future_update (which was marked as an equivalent by @ran_update). Update to at least @future_version_string.', [
|
||||
'%module' => $module_info->info['name'],
|
||||
'@ran_update' => $data['ran_update'],
|
||||
'@future_update' => $future_update,
|
||||
'@future_version_string' => $future_version_string,
|
||||
]),
|
||||
'severity' => REQUIREMENT_ERROR,
|
||||
];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add warning when twig debug option is enabled.
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
name: 'Equivalent Update test'
|
||||
type: module
|
||||
description: 'Support module for update testing.'
|
||||
package: Testing
|
||||
version: VERSION
|
|
@ -0,0 +1,167 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Update hooks and schema definition for the update_test_schema module.
|
||||
*/
|
||||
|
||||
use Drupal\Core\Update\EquivalentUpdate;
|
||||
|
||||
if (\Drupal::state()->get('equivalent_update_test_last_removed', FALSE)) {
|
||||
|
||||
/**
|
||||
* Implements hook_update_last_removed().
|
||||
*/
|
||||
function equivalent_update_test_update_last_removed() {
|
||||
return \Drupal::state()->get('equivalent_update_test_update_last_removed', 100000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema version 100001.
|
||||
*
|
||||
* A regular update.
|
||||
*/
|
||||
function equivalent_update_test_update_100001() {
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
/**
|
||||
* Schema version 100000.
|
||||
*
|
||||
* Used to determine the initial schema version.
|
||||
*/
|
||||
function equivalent_update_test_update_100000() {
|
||||
throw new \Exception('This code should never be reached.');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (\Drupal::state()->get('equivalent_update_test_update_100002', FALSE)) {
|
||||
|
||||
/**
|
||||
* Schema version 100002.
|
||||
*
|
||||
* Tests that the future update 100101 can be marked as an equivalent.
|
||||
*/
|
||||
function equivalent_update_test_update_100002() {
|
||||
\Drupal::service('update.update_hook_registry')->markFutureUpdateEquivalent(100101, '11.1.0');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (\Drupal::state()->get('equivalent_update_test_update_100101', FALSE)) {
|
||||
|
||||
/**
|
||||
* Schema version 100101.
|
||||
*
|
||||
* This update will be skipped due 100002.
|
||||
*/
|
||||
function equivalent_update_test_update_100101() {
|
||||
throw new \Exception('This code should never be reached.');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (\Drupal::state()->get('equivalent_update_test_update_100201', FALSE)) {
|
||||
|
||||
/**
|
||||
* Schema version 100201.
|
||||
*
|
||||
* This update tests that updates can be skipped using inline code.
|
||||
*/
|
||||
function equivalent_update_test_update_100201() {
|
||||
\Drupal::service('update.update_hook_registry')->markFutureUpdateEquivalent(100201, '11.1.0');
|
||||
// Test calling the getEquivalentUpdate() method in an update function to
|
||||
// ensure it correctly determines the update number.
|
||||
$equivalent_update = \Drupal::service('update.update_hook_registry')->getEquivalentUpdate();
|
||||
if ($equivalent_update instanceof EquivalentUpdate) {
|
||||
return $equivalent_update->toSkipMessage();
|
||||
}
|
||||
throw new \Exception('This code should never be reached.');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (\Drupal::state()->get('equivalent_update_test_update_100301', FALSE)) {
|
||||
|
||||
/**
|
||||
* Schema version 100301.
|
||||
*
|
||||
* This update tests that inline code can determine the update number
|
||||
* correctly and return a NULL when it does not match.
|
||||
*/
|
||||
function equivalent_update_test_update_100301() {
|
||||
\Drupal::service('update.update_hook_registry')->markFutureUpdateEquivalent(100302, '11.1.0');
|
||||
// Test calling the getEquivalentUpdate() method in an update function to
|
||||
// ensure it correctly determines the update number.
|
||||
$equivalent_update = \Drupal::service('update.update_hook_registry')->getEquivalentUpdate();
|
||||
if ($equivalent_update instanceof EquivalentUpdate) {
|
||||
throw new \Exception('This code should never be reached.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema version 100302.
|
||||
*
|
||||
* This update will be skipped by 100301.
|
||||
*/
|
||||
function equivalent_update_test_update_100302() {
|
||||
throw new \Exception('This code should never be reached.');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (\Drupal::state()->get('equivalent_update_test_update_100400', FALSE)) {
|
||||
|
||||
/**
|
||||
* Schema version 100400.
|
||||
*
|
||||
* Tests that the future update 100402 can be marked as an equivalent.
|
||||
*/
|
||||
function equivalent_update_test_update_100400() {
|
||||
\Drupal::service('update.update_hook_registry')->markFutureUpdateEquivalent(100402, '11.2.0');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (\Drupal::state()->get('equivalent_update_test_update_100401', FALSE)) {
|
||||
|
||||
/**
|
||||
* Schema version 100401.
|
||||
*
|
||||
* Tests that the future update 100402 can be marked as an equivalent again.
|
||||
*/
|
||||
function equivalent_update_test_update_100401() {
|
||||
\Drupal::service('update.update_hook_registry')->markFutureUpdateEquivalent(100402, '11.2.0');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (\Drupal::state()->get('equivalent_update_test_update_100402', FALSE)) {
|
||||
|
||||
/**
|
||||
* Schema version 100402.
|
||||
*
|
||||
* This update will be skipped by 100400 and 100401.
|
||||
*/
|
||||
function equivalent_update_test_update_100402() {
|
||||
throw new \Exception('This code should never be reached.');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (\Drupal::state()->get('equivalent_update_test_update_100501', FALSE)) {
|
||||
|
||||
/**
|
||||
* Schema version 100501.
|
||||
*
|
||||
* This update will trigger an exception because 100501 is bigger than 100302.
|
||||
*/
|
||||
function equivalent_update_test_update_100501() {
|
||||
\Drupal::service('update.update_hook_registry')->markFutureUpdateEquivalent(100302, '11.1.0');
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,238 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\system\Functional\UpdateSystem;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\Tests\RequirementsPageTrait;
|
||||
|
||||
/**
|
||||
* Tests that update hooks are properly run.
|
||||
*
|
||||
* @group Update
|
||||
*/
|
||||
class EquivalentUpdateTest extends BrowserTestBase {
|
||||
|
||||
use RequirementsPageTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['equivalent_update_test'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* The update URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $updateUrl;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
require_once $this->root . '/core/includes/update.inc';
|
||||
$this->user = $this->drupalCreateUser([
|
||||
'administer software updates',
|
||||
'access site in maintenance mode',
|
||||
]);
|
||||
$this->updateUrl = Url::fromRoute('system.db_update');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that update hooks are properly run.
|
||||
*/
|
||||
public function testUpdateHooks() {
|
||||
/** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
|
||||
$update_registry = \Drupal::service('update.update_hook_registry');
|
||||
$this->drupalLogin($this->user);
|
||||
|
||||
// Verify that the 100000 schema is in place due to
|
||||
// equivalent_update_test_update_100000().
|
||||
$this->assertEquals(100000, $update_registry->getInstalledVersion('equivalent_update_test'));
|
||||
|
||||
// Remove the update and implement hook_update_last_removed().
|
||||
\Drupal::state()->set('equivalent_update_test_last_removed', TRUE);
|
||||
|
||||
$this->drupalGet($this->updateUrl, ['external' => TRUE]);
|
||||
$this->updateRequirementsProblem();
|
||||
$this->clickLink('Continue');
|
||||
$this->assertSession()->pageTextContains('Schema version 100001.');
|
||||
// Run the update hooks.
|
||||
$this->clickLink('Apply pending updates');
|
||||
$this->checkForMetaRefresh();
|
||||
|
||||
// Ensure schema has changed.
|
||||
$this->resetAll();
|
||||
/** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
|
||||
$update_registry = \Drupal::service('update.update_hook_registry');
|
||||
$this->assertEquals(100001, $update_registry->getInstalledVersion('equivalent_update_test'));
|
||||
|
||||
// Set the first update to run.
|
||||
\Drupal::state()->set('equivalent_update_test_update_100002', TRUE);
|
||||
|
||||
$this->drupalGet($this->updateUrl, ['external' => TRUE]);
|
||||
$this->updateRequirementsProblem();
|
||||
$this->clickLink('Continue');
|
||||
$this->assertSession()->pageTextContains('Schema version 100002.');
|
||||
// Run the update hooks.
|
||||
$this->clickLink('Apply pending updates');
|
||||
$this->checkForMetaRefresh();
|
||||
|
||||
// Ensure schema has changed.
|
||||
$this->resetAll();
|
||||
/** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
|
||||
$update_registry = \Drupal::service('update.update_hook_registry');
|
||||
$this->assertEquals(100002, $update_registry->getInstalledVersion('equivalent_update_test'));
|
||||
$this->assertSame(100002, $update_registry->getEquivalentUpdate('equivalent_update_test', 100101)->ran_update);
|
||||
|
||||
\Drupal::state()->set('equivalent_update_test_update_100002', FALSE);
|
||||
\Drupal::state()->set('equivalent_update_test_update_100101', FALSE);
|
||||
|
||||
$this->drupalGet($this->updateUrl, ['external' => TRUE]);
|
||||
$this->updateRequirementsProblem();
|
||||
$this->assertSession()->pageTextContains('The version of the Equivalent Update test module that you are attempting to update to is missing update 100101 (which was marked as an equivalent by 100002). Update to at least Drupal Core 11.1.0.');
|
||||
|
||||
\Drupal::state()->set('equivalent_update_test_update_100101', TRUE);
|
||||
$this->drupalGet($this->updateUrl, ['external' => TRUE]);
|
||||
$this->updateRequirementsProblem();
|
||||
$this->clickLink('Continue');
|
||||
$this->assertSession()->pageTextContains('Schema version 100101.');
|
||||
// Run the update hooks.
|
||||
$this->clickLink('Apply pending updates');
|
||||
$this->checkForMetaRefresh();
|
||||
$this->assertSession()->pageTextContains('Update 100101 for the equivalent_update_test module has been skipped because the equivalent change was already made in update 100002.');
|
||||
|
||||
// Ensure that the schema has changed.
|
||||
$this->resetAll();
|
||||
/** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
|
||||
$update_registry = \Drupal::service('update.update_hook_registry');
|
||||
$this->assertEquals(100101, $update_registry->getInstalledVersion('equivalent_update_test'));
|
||||
$this->assertNull($update_registry->getEquivalentUpdate('equivalent_update_test', 100101));
|
||||
|
||||
\Drupal::state()->set('equivalent_update_test_update_100201', TRUE);
|
||||
$this->drupalGet($this->updateUrl, ['external' => TRUE]);
|
||||
$this->updateRequirementsProblem();
|
||||
$this->clickLink('Continue');
|
||||
$this->assertSession()->pageTextContains('Schema version 100201.');
|
||||
// Run the update hooks.
|
||||
$this->clickLink('Apply pending updates');
|
||||
$this->checkForMetaRefresh();
|
||||
$this->assertSession()->pageTextContains('Update 100201 for the equivalent_update_test module has been skipped because the equivalent change was already made in update 100201.');
|
||||
|
||||
\Drupal::state()->set('equivalent_update_test_update_100301', TRUE);
|
||||
$this->drupalGet($this->updateUrl, ['external' => TRUE]);
|
||||
$this->updateRequirementsProblem();
|
||||
$this->clickLink('Continue');
|
||||
$this->assertSession()->pageTextContains('Schema version 100301.');
|
||||
// Run the update hooks.
|
||||
$this->clickLink('Apply pending updates');
|
||||
$this->checkForMetaRefresh();
|
||||
$this->assertSession()->pageTextContains('Update 100302 for the equivalent_update_test module has been skipped because the equivalent change was already made in update 100301.');
|
||||
|
||||
\Drupal::state()->set('equivalent_update_test_update_100400', TRUE);
|
||||
$this->drupalGet($this->updateUrl, ['external' => TRUE]);
|
||||
$this->updateRequirementsProblem();
|
||||
$this->clickLink('Continue');
|
||||
$this->assertSession()->pageTextContains('Schema version 100400.');
|
||||
// Run the update hooks.
|
||||
$this->clickLink('Apply pending updates');
|
||||
$this->checkForMetaRefresh();
|
||||
|
||||
\Drupal::state()->set('equivalent_update_test_update_100400', FALSE);
|
||||
\Drupal::state()->set('equivalent_update_test_update_100401', TRUE);
|
||||
$this->drupalGet($this->updateUrl, ['external' => TRUE]);
|
||||
$this->updateRequirementsProblem();
|
||||
$this->assertSession()->pageTextContains('The version of the Equivalent Update test module that you are attempting to update to is missing update 100402 (which was marked as an equivalent by 100400). Update to at least Drupal Core 11.2.0.');
|
||||
|
||||
\Drupal::state()->set('equivalent_update_test_update_100400', TRUE);
|
||||
$this->drupalGet($this->updateUrl, ['external' => TRUE]);
|
||||
$this->updateRequirementsProblem();
|
||||
$this->clickLink('Continue');
|
||||
$this->assertSession()->pageTextContains('Schema version 100401.');
|
||||
// Run the update hooks.
|
||||
$this->clickLink('Apply pending updates');
|
||||
$this->checkForMetaRefresh();
|
||||
|
||||
\Drupal::state()->set('equivalent_update_test_update_100400', FALSE);
|
||||
\Drupal::state()->set('equivalent_update_test_update_100401', FALSE);
|
||||
$this->drupalGet($this->updateUrl, ['external' => TRUE]);
|
||||
$this->updateRequirementsProblem();
|
||||
$this->assertSession()->pageTextContains('The version of the Equivalent Update test module that you are attempting to update to is missing update 100402 (which was marked as an equivalent by 100401). Update to at least Drupal Core 11.2.0.');
|
||||
|
||||
\Drupal::state()->set('equivalent_update_test_update_100402', TRUE);
|
||||
$this->drupalGet($this->updateUrl, ['external' => TRUE]);
|
||||
$this->updateRequirementsProblem();
|
||||
$this->clickLink('Continue');
|
||||
$this->assertSession()->pageTextContains('Schema version 100402.');
|
||||
// Run the update hooks.
|
||||
$this->clickLink('Apply pending updates');
|
||||
$this->checkForMetaRefresh();
|
||||
// Ensure that the schema has changed.
|
||||
$this->resetAll();
|
||||
/** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
|
||||
$update_registry = \Drupal::service('update.update_hook_registry');
|
||||
$this->assertEquals(100402, $update_registry->getInstalledVersion('equivalent_update_test'));
|
||||
|
||||
\Drupal::state()->set('equivalent_update_test_update_100501', TRUE);
|
||||
$this->drupalGet($this->updateUrl, ['external' => TRUE]);
|
||||
$this->updateRequirementsProblem();
|
||||
$this->clickLink('Continue');
|
||||
$this->assertSession()->pageTextContains('Schema version 100501.');
|
||||
// Run the update hooks.
|
||||
$this->clickLink('Apply pending updates');
|
||||
$this->checkForMetaRefresh();
|
||||
$this->assertSession()->pageTextContains('LogicException: Cannot mark the update 100302 as an equivalent since it is less than the current update 100501 for the equivalent_update_test module');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that module uninstall removes skipped update information.
|
||||
*/
|
||||
public function testModuleUninstall() {
|
||||
/** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
|
||||
$update_registry = \Drupal::service('update.update_hook_registry');
|
||||
|
||||
// Verify that the 100000 schema is in place due to
|
||||
// equivalent_update_test_update_last_removed().
|
||||
$this->assertEquals(100000, $update_registry->getInstalledVersion('equivalent_update_test'));
|
||||
|
||||
// Set the first update to run.
|
||||
\Drupal::state()->set('equivalent_update_test_update_100002', TRUE);
|
||||
|
||||
$this->drupalLogin($this->user);
|
||||
$this->drupalGet($this->updateUrl, ['external' => TRUE]);
|
||||
$this->updateRequirementsProblem();
|
||||
$this->clickLink('Continue');
|
||||
$this->assertSession()->pageTextContains('Schema version 100002.');
|
||||
// Run the update hooks.
|
||||
$this->clickLink('Apply pending updates');
|
||||
$this->checkForMetaRefresh();
|
||||
|
||||
// Ensure that the schema has changed.
|
||||
$this->resetAll();
|
||||
/** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
|
||||
$update_registry = \Drupal::service('update.update_hook_registry');
|
||||
$this->assertEquals(100002, $update_registry->getInstalledVersion('equivalent_update_test'));
|
||||
$this->assertSame(100002, $update_registry->getEquivalentUpdate('equivalent_update_test', 100101)->ran_update);
|
||||
|
||||
\Drupal::service('module_installer')->uninstall(['equivalent_update_test']);
|
||||
|
||||
$this->assertNull($update_registry->getEquivalentUpdate('equivalent_update_test', 100101));
|
||||
$this->assertEmpty($update_registry->getAllEquivalentUpdates());
|
||||
}
|
||||
|
||||
}
|
|
@ -73,6 +73,11 @@ class UpdateHookRegistryTest extends UnitTestCase {
|
|||
*/
|
||||
protected $keyValueFactory;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface|\PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
protected KeyValueStoreInterface $equivalentUpdatesStore;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
@ -80,11 +85,14 @@ class UpdateHookRegistryTest extends UnitTestCase {
|
|||
parent::setUp();
|
||||
$this->keyValueFactory = $this->createMock(KeyValueFactoryInterface::class);
|
||||
$this->keyValueStore = $this->createMock(KeyValueStoreInterface::class);
|
||||
$this->equivalentUpdatesStore = $this->createMock(KeyValueStoreInterface::class);
|
||||
|
||||
$this->keyValueFactory
|
||||
->method('get')
|
||||
->with('system.schema')
|
||||
->willReturn($this->keyValueStore);
|
||||
->willReturnMap([
|
||||
['system.schema', $this->keyValueStore],
|
||||
['core.equivalent_updates', $this->equivalentUpdatesStore],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue