diff --git a/core/core.services.yml b/core/core.services.yml index c476e6860ad9..de88d8989d0c 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1518,6 +1518,12 @@ services: - { name: placeholder_strategy, priority: -1000 } email.validator: class: Egulias\EmailValidator\EmailValidator + update.post_update_registry: + class: Drupal\Core\Update\UpdateRegistry + factory: ['@update.post_update_registry_factory', create] + update.post_update_registry_factory: + class: Drupal\Core\Update\UpdateRegistryFactory + parent: container.trait response_filter.active_link: class: Drupal\Core\EventSubscriber\ActiveLinkResponseFilter diff --git a/core/includes/update.inc b/core/includes/update.inc index cd1e2aeefed0..683e7fe530cf 100644 --- a/core/includes/update.inc +++ b/core/includes/update.inc @@ -182,6 +182,7 @@ function update_do_one($module, $number, $dependency_map, &$context) { // @TODO We may want to do different error handling for different // exception types, but for now we'll just log the exception and // return the message for printing. + // @see https://www.drupal.org/node/2564311 catch (Exception $e) { watchdog_exception('update', $e); @@ -214,7 +215,66 @@ function update_do_one($module, $number, $dependency_map, &$context) { drupal_set_installed_schema_version($module, $number); } - $context['message'] = 'Updating ' . Html::escape($module) . ' module'; + $context['message'] = t('Updating @module', ['@module' => $module]); +} + +/** + * Executes a single hook_post_update_NAME(). + * + * @param string $function + * The function name, that should be executed. + * @param array $context + * The batch context array. + */ +function update_invoke_post_update($function, &$context) { + $ret = []; + + // If this update was aborted in a previous step, or has a dependency that was + // aborted in a previous step, go no further. + if (!empty($context['results']['#abort'])) { + return; + } + + list($module, $name) = explode('_post_update_', $function, 2); + module_load_include('php', $module, $module . '.post_update'); + if (function_exists($function)) { + try { + $ret['results']['query'] = $function($context['sandbox']); + $ret['results']['success'] = TRUE; + + \Drupal::service('update.post_update_registry')->registerInvokedUpdates([$function]); + } + // @TODO We may want to do different error handling for different exception + // types, but for now we'll just log the exception and return the message + // for printing. + // @see https://www.drupal.org/node/2564311 + catch (Exception $e) { + watchdog_exception('update', $e); + + $variables = Error::decodeException($e); + unset($variables['backtrace']); + $ret['#abort'] = [ + 'success' => FALSE, + 'query' => t('%type: @message in %function (line %line of %file).', $variables), + ]; + } + } + + if (isset($context['sandbox']['#finished'])) { + $context['finished'] = $context['sandbox']['#finished']; + unset($context['sandbox']['#finished']); + } + if (!isset($context['results'][$module][$name])) { + $context['results'][$module][$name] = array(); + } + $context['results'][$module][$name] = array_merge($context['results'][$module][$name], $ret); + + if (!empty($ret['#abort'])) { + // Record this function in the list of updates that were aborted. + $context['results']['#abort'][] = $function; + } + + $context['message'] = t('Post updating @module', ['@module' => $module]); } /** diff --git a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php index 3238ce65ea9b..c088eedb7b52 100644 --- a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php @@ -90,15 +90,30 @@ class ExtensionDiscovery { */ protected $fileCache; + /** + * The site path. + * + * @var string + */ + protected $sitePath; + /** * Constructs a new ExtensionDiscovery object. * * @param string $root * The app root. + * @param bool $use_file_cache + * Whether file cache should be used. + * @param string[] $profile_directories + * The available profile directories + * @param string $site_path + * The path to the site. */ - public function __construct($root) { + public function __construct($root, $use_file_cache = TRUE, $profile_directories = NULL, $site_path = NULL) { $this->root = $root; - $this->fileCache = FileCacheFactory::get('extension_discovery'); + $this->fileCache = $use_file_cache ? FileCacheFactory::get('extension_discovery') : NULL; + $this->profileDirectories = $profile_directories; + $this->sitePath = $site_path; } /** @@ -172,7 +187,7 @@ class ExtensionDiscovery { $searchdirs[static::ORIGIN_SITE] = \Drupal::service('site.path'); } else { - $searchdirs[static::ORIGIN_SITE] = DrupalKernel::findSitePath(Request::createFromGlobals()); + $searchdirs[static::ORIGIN_SITE] = $this->sitePath ?: DrupalKernel::findSitePath(Request::createFromGlobals()); } // Unless an explicit value has been passed, manually check whether we are @@ -180,7 +195,7 @@ class ExtensionDiscovery { // Test extensions can also be included for debugging purposes by setting a // variable in settings.php. if (!isset($include_tests)) { - $include_tests = drupal_valid_test_ua() || Settings::get('extension_discovery_scan_tests'); + $include_tests = Settings::get('extension_discovery_scan_tests') || drupal_valid_test_ua(); } $files = array(); @@ -427,7 +442,7 @@ class ExtensionDiscovery { continue; } - if ($cached_extension = $this->fileCache->get($fileinfo->getPathName())) { + if ($this->fileCache && $cached_extension = $this->fileCache->get($fileinfo->getPathName())) { $files[$cached_extension->getType()][$key] = $cached_extension; continue; } @@ -467,7 +482,10 @@ class ExtensionDiscovery { $extension->origin = $dir; $files[$type][$key] = $extension; - $this->fileCache->set($fileinfo->getPathName(), $extension); + + if ($this->fileCache) { + $this->fileCache->set($fileinfo->getPathName(), $extension); + } } return $files; } diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php index a8124d5242cb..92bdb801ca4a 100644 --- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php +++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php @@ -260,6 +260,11 @@ class ModuleInstaller implements ModuleInstallerInterface { } drupal_set_installed_schema_version($module, $version); + // Ensure that all post_update functions are registered already. + /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */ + $post_update_registry = \Drupal::service('update.post_update_registry'); + $post_update_registry->registerInvokedUpdates($post_update_registry->getModuleUpdateFunctions($module)); + // Record the fact that it was installed. $modules_installed[] = $module; @@ -445,6 +450,10 @@ class ModuleInstaller implements ModuleInstallerInterface { $schema_store = \Drupal::keyValue('system.schema'); $schema_store->delete($module); + + /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */ + $post_update_registry = \Drupal::service('update.post_update_registry'); + $post_update_registry->filterOutInvokedUpdatesByModule($module); } \Drupal::service('router.builder')->setRebuildNeeded(); drupal_get_installed_schema_version(NULL, TRUE); diff --git a/core/lib/Drupal/Core/Extension/module.api.php b/core/lib/Drupal/Core/Extension/module.api.php index d7d5332d9ff8..2abf19658f3d 100644 --- a/core/lib/Drupal/Core/Extension/module.api.php +++ b/core/lib/Drupal/Core/Extension/module.api.php @@ -659,6 +659,71 @@ function hook_update_N(&$sandbox) { return t('All foo bars were updated with the new suffix'); } +/** + * Executes an update which is intended to update data, like entities. + * + * These implementations have to be placed in a MODULE.post_update.php file. + * + * These updates are executed after all hook_update_N() implementations. At this + * stage Drupal is already fully repaired so you can use any API as you wish. + * + * NAME can be arbitrary machine names. In contrast to hook_update_N() the order + * of functions in the file is the only thing which ensures the execution order + * of those functions. + * + * Drupal also ensures to not execute the same hook_post_update_NAME() function + * twice. + * + * @param array $sandbox + * Stores information for batch updates. See above for more information. + * + * @throws \Drupal\Core\Utility\UpdateException|PDOException + * In case of error, update hooks should throw an instance of + * \Drupal\Core\Utility\UpdateException with a meaningful message for the + * user. If a database query fails for whatever reason, it will throw a + * PDOException. + * + * @return string|null + * Optionally, hook_post_update_NAME() hooks may return a translated string + * that will be displayed to the user after the update has completed. If no + * message is returned, no message will be presented to the user. + * + * @ingroup update_api + * + * @see hook_update_N() + */ +function hook_post_update_NAME(&$sandbox) { + // Example of updating some content. + $node = \Drupal\node\Entity\Node::load(123); + $node->setTitle('foo'); + $node->save(); + + $result = t('Node %nid saved', ['%nid' => $node->id()]); + + // Example of disabling blocks with missing condition contexts. Note: The + // block itself is in a state which is valid at that point. + // @see block_update_8001() + // @see block_post_update_disable_blocks_with_missing_contexts() + $block_update_8001 = \Drupal::keyValue('update_backup')->get('block_update_8001', []); + + $block_ids = array_keys($block_update_8001); + $block_storage = \Drupal::entityManager()->getStorage('block'); + $blocks = $block_storage->loadMultiple($block_ids); + /** @var $blocks \Drupal\block\BlockInterface[] */ + foreach ($blocks as $block) { + // This block has had conditions removed due to an inability to resolve + // contexts in block_update_8001() so disable it. + + // Disable currently enabled blocks. + if ($block_update_8001[$block->id()]['status']) { + $block->setStatus(FALSE); + $block->save(); + } + } + + return $result; +} + /** * Return an array of information about module update dependencies. * diff --git a/core/lib/Drupal/Core/Update/UpdateRegistry.php b/core/lib/Drupal/Core/Update/UpdateRegistry.php new file mode 100644 index 000000000000..0190e04b4d78 --- /dev/null +++ b/core/lib/Drupal/Core/Update/UpdateRegistry.php @@ -0,0 +1,267 @@ +root = $root; + $this->sitePath = $site_path; + $this->enabledModules = $enabled_modules; + $this->keyValue = $key_value; + $this->includeTests = $include_tests; + } + + /** + * Gets all available update functions. + * + * @return callable[] + * A list of update functions. + */ + protected function getAvailableUpdateFunctions() { + $regexp = '/^(?.+)_' . $this->updateType . '_(?.+)$/'; + $functions = get_defined_functions(); + + $updates = []; + foreach (preg_grep('/_' . $this->updateType . '_/', $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 (in_array($matches['module'], $this->enabledModules)) { + $updates[] = $matches['module'] . '_' . $this->updateType . '_' . $matches['name']; + } + } + } + + return $updates; + } + + /** + * Find all update functions that haven't been executed. + * + * @return callable[] + * A list of update functions. + */ + public function getPendingUpdateFunctions() { + // We need a) the list of active modules (we get that from the config + // bootstrap factory) and b) the path to the modules, we use the extension + // discovery for that. + + $this->scanExtensionsAndLoadUpdateFiles(); + + // First figure out which hook_{$this->updateType}_NAME got executed + // already. + $existing_update_functions = $this->keyValue->get('existing_updates', []); + + $available_update_functions = $this->getAvailableUpdateFunctions(); + $not_executed_update_functions = array_diff($available_update_functions, $existing_update_functions); + + return $not_executed_update_functions; + } + + /** + * Loads all update files for a given list of extension. + * + * @param \Drupal\Core\Extension\Extension[] $module_extensions + * The extensions used for loading. + */ + protected function loadUpdateFiles(array $module_extensions) { + // Load all the {$this->updateType}.php files. + foreach ($this->enabledModules as $module) { + if (isset($module_extensions[$module])) { + $this->loadUpdateFile($module_extensions[$module]); + } + } + } + + /** + * Loads the {$this->updateType}.php file for a given extension. + * + * @param \Drupal\Core\Extension\Extension $module + * The extension of the module to load its file. + */ + protected function loadUpdateFile(Extension $module) { + $filename = $this->root . '/' . $module->getPath() . '/' . $module->getName() . ".{$this->updateType}.php"; + if (file_exists($filename)) { + include_once $filename; + } + } + + /** + * Returns a list of all the pending updates. + * + * @return array[] + * An associative array keyed by module name which contains all information + * about database updates that need to be run, and any updates that are not + * going to proceed due to missing requirements. + * + * The subarray for each module can contain the following keys: + * - start: The starting update that is to be processed. If this does not + * exist then do not process any updates for this module as there are + * other requirements that need to be resolved. + * - pending: An array of all the pending updates for the module including + * the description from source code comment for each update function. + * This array is keyed by the update name. + */ + public function getPendingUpdateInformation() { + $functions = $this->getPendingUpdateFunctions(); + + $ret = []; + foreach ($functions as $function) { + list($module, $update) = explode("_{$this->updateType}_", $function); + // The description for an update comes from its Doxygen. + $func = new \ReflectionFunction($function); + $description = trim(str_replace(array("\n", '*', '/'), '', $func->getDocComment()), ' '); + $ret[$module]['pending'][$update] = $description; + if (!isset($ret[$module]['start'])) { + $ret[$module]['start'] = $update; + } + } + return $ret; + } + + /** + * Registers that update fucntions got executed. + * + * @param string[] $function_names + * The executed update functions. + * + * @return $this + */ + public function registerInvokedUpdates(array $function_names) { + $executed_updates = $this->keyValue->get('existing_updates', []); + $executed_updates = array_merge($executed_updates, $function_names); + $this->keyValue->set('existing_updates', $executed_updates); + + return $this; + } + + /** + * Returns all available updates for a given module. + * + * @param string $module_name + * The module name. + * + * @return callable[] + * A list of update functions. + */ + public function getModuleUpdateFunctions($module_name) { + $this->scanExtensionsAndLoadUpdateFiles(); + $all_functions = $this->getAvailableUpdateFunctions(); + + return array_filter($all_functions, function($function_name) use ($module_name) { + list($function_module_name, ) = explode("_{$this->updateType}_", $function_name); + return $function_module_name === $module_name; + }); + } + + /** + * Scans all module + profile extensions and load the update files. + */ + protected function scanExtensionsAndLoadUpdateFiles() { + // Scan the module list. + $extension_discovery = new ExtensionDiscovery($this->root, FALSE, [], $this->sitePath); + $module_extensions = $extension_discovery->scan('module'); + + $profile_extensions = $extension_discovery->scan('profile'); + $extensions = array_merge($module_extensions, $profile_extensions); + + $this->loadUpdateFiles($extensions); + } + + /** + * Filters out already executed update functions by module. + * + * @param string $module + * The module name. + */ + public function filterOutInvokedUpdatesByModule($module) { + $existing_update_functions = $this->keyValue->get('existing_updates', []); + + $remaining_update_functions = array_filter($existing_update_functions, function($function_name) use ($module) { + return strpos($function_name, "{$module}_{$this->updateType}_") !== 0; + }); + + $this->keyValue->set('existing_updates', array_values($remaining_update_functions)); + } + +} diff --git a/core/lib/Drupal/Core/Update/UpdateRegistryFactory.php b/core/lib/Drupal/Core/Update/UpdateRegistryFactory.php new file mode 100644 index 000000000000..ef41154ff00e --- /dev/null +++ b/core/lib/Drupal/Core/Update/UpdateRegistryFactory.php @@ -0,0 +1,30 @@ +container->get('app.root'), $this->container->get('site.path'), array_keys($this->container->get('module_handler')->getModuleList()), $this->container->get('keyvalue')->get('post_update')); + } + +} diff --git a/core/modules/block/block.install b/core/modules/block/block.install index 99ce4ef5ef8b..9abe82efba42 100644 --- a/core/modules/block/block.install +++ b/core/modules/block/block.install @@ -45,8 +45,9 @@ function block_update_8001() { } // Contributed modules should leverage hook_update_dependencies() in order to - // be executed before block_update_8002(), so they can update their context - // mappings, if wanted. + // be executed after block_update_8001(). The blocks are then disabled if the + // contexts are still missing via + // block_post_update_disable_blocks_with_missing_contexts(). $config_factory = \Drupal::configFactory(); $backup_values = $update_backup = []; @@ -95,50 +96,10 @@ function block_update_8001() { } /** - * Disable all blocks with missing context IDs in block_update_8001(). + * Placeholder for the previous 8002 update. */ function block_update_8002() { - $block_update_8001 = \Drupal::keyValue('update_backup')->get('block_update_8001', []); - - $block_ids = array_keys($block_update_8001); - $config_factory = \Drupal::configFactory(); - /** @var \Drupal\Core\Config\Config[] $blocks */ - $blocks = []; - foreach ($block_ids as $block_id) { - $blocks[$block_id] = $block = $config_factory->getEditable('block.block.' . $block_id); - // This block will have an invalid context mapping service and must be - // disabled in order to prevent information disclosure. - - // Disable currently enabled blocks. - if ($block_update_8001[$block_id]['status']) { - $block->set('status', FALSE); - $block->save(TRUE); - } - } - - // Provides a list of plugin labels, keyed by plugin ID. - $condition_plugin_id_label_map = array_column(\Drupal::service('plugin.manager.condition')->getDefinitions(), 'label', 'id'); - - // Override with the UI labels we are aware of. Sadly they are not machine - // accessible, see - // \Drupal\node\Plugin\Condition\NodeType::buildConfigurationForm(). - $condition_plugin_id_label_map['node_type'] = t('Content types'); - $condition_plugin_id_label_map['request_path'] = t('Pages'); - $condition_plugin_id_label_map['user_role'] = t('Roles'); - - if (count($block_ids) > 0) { - $message = t('Encountered an unknown context mapping key coming probably from a contributed or custom module: One or more mappings could not be updated. Please manually review your visibility settings for the following blocks, which are disabled now:'); - $message .= ''; - - return $message; - } + \Drupal::state()->set('block_update_8002_placeholder', TRUE); } /** diff --git a/core/modules/block/block.post_update.php b/core/modules/block/block.post_update.php new file mode 100644 index 000000000000..f208f6518d6c --- /dev/null +++ b/core/modules/block/block.post_update.php @@ -0,0 +1,79 @@ += 8002 && !\Drupal::state()->get('block_update_8002_placeholder', FALSE)) { + return; + } + + // Cleanup the state entry as its no longer needed. + \Drupal::state()->delete('block_update_8002'); + + $block_update_8001 = \Drupal::keyValue('update_backup')->get('block_update_8001', []); + + $block_ids = array_keys($block_update_8001); + $block_storage = \Drupal::entityManager()->getStorage('block'); + $blocks = $block_storage->loadMultiple($block_ids); + /** @var $blocks \Drupal\block\BlockInterface[] */ + foreach ($blocks as $block) { + // This block has had conditions removed due to an inability to resolve + // contexts in block_update_8001() so disable it. + + // Disable currently enabled blocks. + if ($block_update_8001[$block->id()]['status']) { + $block->setStatus(FALSE); + $block->save(); + } + } + + // Provides a list of plugin labels, keyed by plugin ID. + $condition_plugin_id_label_map = array_column(\Drupal::service('plugin.manager.condition')->getDefinitions(), 'label', 'id'); + + // Override with the UI labels we are aware of. Sadly they are not machine + // accessible, see + // \Drupal\node\Plugin\Condition\NodeType::buildConfigurationForm(). + $condition_plugin_id_label_map['node_type'] = t('Content types'); + $condition_plugin_id_label_map['request_path'] = t('Pages'); + $condition_plugin_id_label_map['user_role'] = t('Roles'); + + if (count($block_ids) > 0) { + $message = t('Encountered an unknown context mapping key coming probably from a contributed or custom module: One or more mappings could not be updated. Please manually review your visibility settings for the following blocks, which are disabled now:'); + $message .= ''; + + return $message; + } +} + +/** + * @} End of "addtogroup updates-8.0.0-beta". + */ diff --git a/core/modules/system/src/Controller/DbUpdateController.php b/core/modules/system/src/Controller/DbUpdateController.php index fa8104d6c175..61e89a248f41 100644 --- a/core/modules/system/src/Controller/DbUpdateController.php +++ b/core/modules/system/src/Controller/DbUpdateController.php @@ -15,6 +15,7 @@ use Drupal\Core\Render\BareHtmlPageRendererInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Site\Settings; use Drupal\Core\State\StateInterface; +use Drupal\Core\Update\UpdateRegistry; use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Response; @@ -74,6 +75,13 @@ class DbUpdateController extends ControllerBase { */ protected $root; + /** + * The post update registry. + * + * @var \Drupal\Core\Update\UpdateRegistry + */ + protected $postUpdateRegistry; + /** * Constructs a new UpdateController. * @@ -91,8 +99,10 @@ class DbUpdateController extends ControllerBase { * The current user. * @param \Drupal\Core\Render\BareHtmlPageRendererInterface $bare_html_page_renderer * The bare HTML page renderer. + * @param \Drupal\Core\Update\UpdateRegistry $post_update_registry + * The post update registry. */ - public function __construct($root, KeyValueExpirableFactoryInterface $key_value_expirable_factory, CacheBackendInterface $cache, StateInterface $state, ModuleHandlerInterface $module_handler, AccountInterface $account, BareHtmlPageRendererInterface $bare_html_page_renderer) { + public function __construct($root, KeyValueExpirableFactoryInterface $key_value_expirable_factory, CacheBackendInterface $cache, StateInterface $state, ModuleHandlerInterface $module_handler, AccountInterface $account, BareHtmlPageRendererInterface $bare_html_page_renderer, UpdateRegistry $post_update_registry) { $this->root = $root; $this->keyValueExpirableFactory = $key_value_expirable_factory; $this->cache = $cache; @@ -100,6 +110,7 @@ class DbUpdateController extends ControllerBase { $this->moduleHandler = $module_handler; $this->account = $account; $this->bareHtmlPageRenderer = $bare_html_page_renderer; + $this->postUpdateRegistry = $post_update_registry; } /** @@ -113,7 +124,8 @@ class DbUpdateController extends ControllerBase { $container->get('state'), $container->get('module_handler'), $container->get('current_user'), - $container->get('bare_html_page_renderer') + $container->get('bare_html_page_renderer'), + $container->get('update.post_update_registry') ); } @@ -257,35 +269,52 @@ class DbUpdateController extends ControllerBase { // Ensure system.module's updates appear first. $build['start']['system'] = array(); - $updates = update_get_update_list(); $starting_updates = array(); $incompatible_updates_exist = FALSE; - foreach ($updates as $module => $update) { - if (!isset($update['start'])) { - $build['start'][$module] = array( - '#type' => 'item', - '#title' => $module . ' module', - '#markup' => $update['warning'], - '#prefix' => '
', - '#suffix' => '
', - ); - $incompatible_updates_exist = TRUE; - continue; + $updates_per_module = []; + foreach (['update', 'post_update'] as $update_type) { + switch ($update_type) { + case 'update': + $updates = update_get_update_list(); + break; + case 'post_update': + $updates = $this->postUpdateRegistry->getPendingUpdateInformation(); + break; } - if (!empty($update['pending'])) { - $starting_updates[$module] = $update['start']; - $build['start'][$module] = array( - '#type' => 'hidden', - '#value' => $update['start'], - ); - $build['start'][$module . '_updates'] = array( - '#theme' => 'item_list', - '#items' => $update['pending'], - '#title' => $module . ' module', - ); - } - if (isset($update['pending'])) { - $count = $count + count($update['pending']); + foreach ($updates as $module => $update) { + if (!isset($update['start'])) { + $build['start'][$module] = array( + '#type' => 'item', + '#title' => $module . ' module', + '#markup' => $update['warning'], + '#prefix' => '
', + '#suffix' => '
', + ); + $incompatible_updates_exist = TRUE; + continue; + } + if (!empty($update['pending'])) { + $updates_per_module += [$module => []]; + $updates_per_module[$module] = array_merge($updates_per_module[$module], $update['pending']); + $build['start'][$module] = array( + '#type' => 'hidden', + '#value' => $update['start'], + ); + // Store the previous items in order to merge normal updates and + // post_update functions together. + $build['start'][$module] = array( + '#theme' => 'item_list', + '#items' => $updates_per_module[$module], + '#title' => $module . ' module', + ); + + if ($update_type === 'update') { + $starting_updates[$module] = $update['start']; + } + } + if (isset($update['pending'])) { + $count = $count + count($update['pending']); + } } } @@ -415,7 +444,7 @@ class DbUpdateController extends ControllerBase { if ($module != '#abort') { $module_has_message = FALSE; $info_messages = array(); - foreach ($updates as $number => $queries) { + foreach ($updates as $name => $queries) { $messages = array(); foreach ($queries as $query) { // If there is no message for this update, don't show anything. @@ -439,10 +468,16 @@ class DbUpdateController extends ControllerBase { if ($messages) { $module_has_message = TRUE; + if (is_numeric($name)) { + $title = $this->t('Update #@count', ['@count' => $name]); + } + else { + $title = $this->t('Update @name', ['@name' => trim($name, '_')]); + } $info_messages[] = array( '#theme' => 'item_list', '#items' => $messages, - '#title' => $this->t('Update #@count', array('@count' => $number)), + '#title' => $title, ); } } @@ -575,6 +610,17 @@ class DbUpdateController extends ControllerBase { } } + $post_updates = $this->postUpdateRegistry->getPendingUpdateFunctions(); + + if ($post_updates) { + // Now we rebuild all caches and after that execute the hook_post_update() + // functions. + $operations[] = ['drupal_flush_all_caches', []]; + foreach ($post_updates as $function) { + $operations[] = ['update_invoke_post_update', [$function]]; + } + } + $batch['operations'] = $operations; $batch += array( 'title' => $this->t('Updating'), diff --git a/core/modules/system/src/Tests/Module/InstallTest.php b/core/modules/system/src/Tests/Module/InstallTest.php index ad81180cf808..5f1a091d3dae 100644 --- a/core/modules/system/src/Tests/Module/InstallTest.php +++ b/core/modules/system/src/Tests/Module/InstallTest.php @@ -53,6 +53,21 @@ class InstallTest extends WebTestBase { $this->assertTrue($version > 0, 'System module version is > 0.'); $version = drupal_get_installed_schema_version('user', TRUE); $this->assertTrue($version > 0, 'User module version is > 0.'); + + $post_update_key_value = \Drupal::keyValue('post_update'); + $existing_updates = $post_update_key_value->get('existing_updates', []); + $this->assertTrue(in_array('module_test_post_update_test', $existing_updates)); + } + + /** + * Ensures that post update functions are removed on uninstall. + */ + public function testUninstallPostUpdateFunctions() { + \Drupal::service('module_installer')->uninstall(['module_test']); + + $post_update_key_value = \Drupal::keyValue('post_update'); + $existing_updates = $post_update_key_value->get('existing_updates', []); + $this->assertFalse(in_array('module_test_post_update_test', $existing_updates)); } /** diff --git a/core/modules/system/src/Tests/Module/InstallUninstallTest.php b/core/modules/system/src/Tests/Module/InstallUninstallTest.php index 2295b75fc05c..24549b5048bd 100644 --- a/core/modules/system/src/Tests/Module/InstallUninstallTest.php +++ b/core/modules/system/src/Tests/Module/InstallUninstallTest.php @@ -16,7 +16,10 @@ use Drupal\Core\Logger\RfcLogLevel; */ class InstallUninstallTest extends ModuleTestBase { - public static $modules = array('system_test', 'dblog', 'taxonomy'); + /** + * {@inheritdoc} + */ + public static $modules = array('system_test', 'dblog', 'taxonomy', 'update_test_postupdate'); /** * Tests that a fixed set of modules can be installed and uninstalled. @@ -105,6 +108,7 @@ class InstallUninstallTest extends ModuleTestBase { $this->assertModuleTablesExist($module_to_install); $this->assertModuleConfig($module_to_install); $this->assertLogMessage('system', "%module module installed.", array('%module' => $module_to_install), RfcLogLevel::INFO); + $this->assertInstallModuleUpdates($module_to_install); } // Uninstall the original module, and check appropriate @@ -188,6 +192,59 @@ class InstallUninstallTest extends ModuleTestBase { $this->assertModuleTablesDoNotExist($module); // Check that the module's config files no longer exist. $this->assertNoModuleConfig($module); + $this->assertUninstallModuleUpdates($module); + } + + /** + * Asserts the module post update functions after install. + * + * @param string $module + * The module that got installed. + */ + protected function assertInstallModuleUpdates($module) { + /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */ + $post_update_registry = \Drupal::service('update.post_update_registry'); + $all_update_functions = $post_update_registry->getPendingUpdateFunctions(); + $empty_result = TRUE; + foreach ($all_update_functions as $function) { + list($function_module, ) = explode('_post_update_', $function); + if ($module === $function_module) { + $empty_result = FALSE; + break; + } + } + $this->assertTrue($empty_result, 'Ensures that no pending post update functions are available.'); + + $existing_updates = \Drupal::keyValue('post_update')->get('existing_updates', []); + switch ($module) { + case 'block': + $this->assertFalse(array_diff(['block_post_update_disable_blocks_with_missing_contexts'], $existing_updates)); + break; + case 'update_test_postupdate': + $this->assertFalse(array_diff(['update_test_postupdate_post_update_first', 'update_test_postupdate_post_update_second', 'update_test_postupdate_post_update_test1', 'update_test_postupdate_post_update_test0'], $existing_updates)); + break; + } + } + + /** + * Asserts the module post update functions after uninstall. + * + * @param string $module + * The module that got installed. + */ + protected function assertUninstallModuleUpdates($module) { + /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */ + $post_update_registry = \Drupal::service('update.post_update_registry'); + $all_update_functions = $post_update_registry->getPendingUpdateFunctions(); + + switch ($module) { + case 'block': + $this->assertFalse(array_intersect(['block_post_update_disable_blocks_with_missing_contexts'], $all_update_functions), 'Asserts that no pending post update functions are available.'); + + $existing_updates = \Drupal::keyValue('post_update')->get('existing_updates', []); + $this->assertFalse(array_intersect(['block_post_update_disable_blocks_with_missing_contexts'], $existing_updates), 'Asserts that no post update functions are stored in keyvalue store.'); + break; + } } } diff --git a/core/modules/system/src/Tests/System/StatusTest.php b/core/modules/system/src/Tests/System/StatusTest.php index 57ef4b0e678f..5bb9578abdfa 100644 --- a/core/modules/system/src/Tests/System/StatusTest.php +++ b/core/modules/system/src/Tests/System/StatusTest.php @@ -18,6 +18,11 @@ use Drupal\system\SystemRequirements; */ class StatusTest extends WebTestBase { + /** + * {@inheritdoc} + */ + public static $modules = ['update_test_postupdate']; + /** * {@inheritdoc} */ @@ -57,6 +62,23 @@ class StatusTest extends WebTestBase { $this->assertNoLinkByHref(Url::fromRoute('system.php')->toString()); } + // If a module is fully installed no pending updates exists. + $this->assertNoText(t('Out of date')); + + // Set the schema version of update_test_postupdate to a lower version, so + // update_test_postupdate_update_8001() needs to be executed. + drupal_set_installed_schema_version('update_test_postupdate', 8000); + $this->drupalGet('admin/reports/status'); + $this->assertText(t('Out of date')); + + // Now cleanup the executed post update functions. + drupal_set_installed_schema_version('update_test_postupdate', 8001); + /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */ + $post_update_registry = \Drupal::service('update.post_update_registry'); + $post_update_registry->filterOutInvokedUpdatesByModule('update_test_postupdate'); + $this->drupalGet('admin/reports/status'); + $this->assertText(t('Out of date')); + $this->drupalGet('admin/reports/status/php'); $this->assertResponse(200, 'The phpinfo page is reachable.'); } diff --git a/core/modules/system/src/Tests/Update/UpdatePathTestBase.php b/core/modules/system/src/Tests/Update/UpdatePathTestBase.php index 1b8509884063..c6281d000ee2 100644 --- a/core/modules/system/src/Tests/Update/UpdatePathTestBase.php +++ b/core/modules/system/src/Tests/Update/UpdatePathTestBase.php @@ -123,6 +123,13 @@ abstract class UpdatePathTestBase extends WebTestBase { */ protected $strictConfigSchema = FALSE; + /** + * Fail the test if there are failed updates. + * + * @var bool + */ + protected $checkFailedUpdates = TRUE; + /** * Constructs an UpdatePathTestCase object. * @@ -246,7 +253,9 @@ abstract class UpdatePathTestBase extends WebTestBase { $this->clickLink(t('Apply pending updates')); // Ensure there are no failed updates. - $this->assertNoRaw('' . t('Failed:') . ''); + if ($this->checkFailedUpdates) { + $this->assertNoRaw('' . t('Failed:') . ''); + } // The config schema can be incorrect while the update functions are being // executed. But once the update has been completed, it needs to be valid diff --git a/core/modules/system/src/Tests/Update/UpdatePostUpdateFailingTest.php b/core/modules/system/src/Tests/Update/UpdatePostUpdateFailingTest.php new file mode 100644 index 000000000000..0555871a983a --- /dev/null +++ b/core/modules/system/src/Tests/Update/UpdatePostUpdateFailingTest.php @@ -0,0 +1,53 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../tests/fixtures/update/drupal-8.bare.standard.php.gz', + __DIR__ . '/../../../tests/fixtures/update/drupal-8.update-test-postupdate-failing-enabled.php', + ]; + } + + /** + * Tests hook_post_update_NAME(). + */ + public function testPostUpdate() { + // There are expected to be failed updates. + $this->checkFailedUpdates = FALSE; + + $this->runUpdates(); + + // There should be no post update hooks registered as being run. + $this->assertIdentical([], \Drupal::state()->get('post_update_test_execution', [])); + + $key_value = \Drupal::keyValue('update__post_update'); + $this->assertEqual([], $key_value->get('existing_updates')); + } + + /** + * {@inheritdoc} + */ + protected function doSelectionTest() { + // First update, should not be run since this module's update hooks fail. + $this->assertRaw('8001 - This update will fail.'); + $this->assertRaw('8002 - A further update.'); + $this->assertEscaped("First update, should not be run since this module's update hooks fail."); + } + +} diff --git a/core/modules/system/src/Tests/Update/UpdatePostUpdateTest.php b/core/modules/system/src/Tests/Update/UpdatePostUpdateTest.php new file mode 100644 index 000000000000..3431515ac1d6 --- /dev/null +++ b/core/modules/system/src/Tests/Update/UpdatePostUpdateTest.php @@ -0,0 +1,69 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../tests/fixtures/update/drupal-8.bare.standard.php.gz', + __DIR__ . '/../../../tests/fixtures/update/drupal-8.update-test-postupdate-enabled.php', + ]; + } + + /** + * {@inheritdoc} + */ + protected function doSelectionTest() { + parent::doSelectionTest(); + + // Ensure that normal and post_update updates are merged together on the + // selection page. + $this->assertRaw(''); + } + + /** + * Tests hook_post_update_NAME(). + */ + public function testPostUpdate() { + $this->runUpdates(); + + $this->assertRaw('

Update first

'); + $this->assertRaw('First update'); + $this->assertRaw('

Update second

'); + $this->assertRaw('Second update'); + $this->assertRaw('

Update test1

'); + $this->assertRaw('Test1 update'); + $this->assertRaw('

Update test0

'); + $this->assertRaw('Test0 update'); + + $updates = [ + 'update_test_postupdate_post_update_first', + 'update_test_postupdate_post_update_second', + 'update_test_postupdate_post_update_test1', + 'update_test_postupdate_post_update_test0', + ]; + $this->assertIdentical($updates, \Drupal::state()->get('post_update_test_execution', [])); + + $key_value = \Drupal::keyValue('post_update'); + array_unshift($updates, 'block_post_update_disable_blocks_with_missing_contexts'); + $this->assertEqual($updates, $key_value->get('existing_updates')); + + $this->drupalGet('update.php/selection'); + $this->assertText('No pending updates.'); + } + +} diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 86365a94cc51..c3168844f7f6 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -598,18 +598,31 @@ function system_requirements($phase) { ); // Check installed modules. + $has_pending_updates = FALSE; foreach (\Drupal::moduleHandler()->getModuleList() as $module => $filename) { $updates = drupal_get_schema_versions($module); if ($updates !== FALSE) { $default = drupal_get_installed_schema_version($module); if (max($updates) > $default) { - $requirements['update']['severity'] = REQUIREMENT_ERROR; - $requirements['update']['value'] = t('Out of date'); - $requirements['update']['description'] = t('Some modules have database schema updates to install. You should run the database update script immediately.', array('@update' => \Drupal::url('system.db_update'))); + $has_pending_updates = TRUE; break; } } } + if (!$has_pending_updates) { + /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */ + $post_update_registry = \Drupal::service('update.post_update_registry'); + $missing_post_update_functions = $post_update_registry->getPendingUpdateFunctions(); + if (!empty($missing_post_update_functions)) { + $has_pending_updates = TRUE; + } + } + + if ($has_pending_updates) { + $requirements['update']['severity'] = REQUIREMENT_ERROR; + $requirements['update']['value'] = t('Out of date'); + $requirements['update']['description'] = t('Some modules have database schema updates to install. You should run the database update script immediately.', array('@update' => \Drupal::url('system.db_update'))); + } // Verify that no entity updates are pending after running every DB update. if (!isset($requirements['update']['severity']) && \Drupal::entityDefinitionUpdateManager()->needsUpdates()) { diff --git a/core/modules/system/tests/fixtures/update/drupal-8.update-test-postupdate-enabled.php b/core/modules/system/tests/fixtures/update/drupal-8.update-test-postupdate-enabled.php new file mode 100644 index 000000000000..375b50ddb4e7 --- /dev/null +++ b/core/modules/system/tests/fixtures/update/drupal-8.update-test-postupdate-enabled.php @@ -0,0 +1,40 @@ +merge('key_value') + ->condition('collection', 'system.schema') + ->condition('name', 'update_test_postupdate') + ->fields([ + 'collection' => 'system.schema', + 'name' => 'update_test_postupdate', + 'value' => 'i:8000;', + ]) + ->execute(); + +// Update core.extension. +$extensions = $connection->select('config') + ->fields('config', ['data']) + ->condition('collection', '') + ->condition('name', 'core.extension') + ->execute() + ->fetchField(); +$extensions = unserialize($extensions); +$extensions['module']['update_test_postupdate'] = 8000; +$connection->update('config') + ->fields([ + 'data' => serialize($extensions), + ]) + ->condition('collection', '') + ->condition('name', 'core.extension') + ->execute(); + diff --git a/core/modules/system/tests/fixtures/update/drupal-8.update-test-postupdate-failing-enabled.php b/core/modules/system/tests/fixtures/update/drupal-8.update-test-postupdate-failing-enabled.php new file mode 100644 index 000000000000..f19a1f10878c --- /dev/null +++ b/core/modules/system/tests/fixtures/update/drupal-8.update-test-postupdate-failing-enabled.php @@ -0,0 +1,39 @@ +merge('key_value') + ->condition('collection', 'system.schema') + ->condition('name', 'update_test_failing') + ->fields([ + 'collection' => 'system.schema', + 'name' => 'update_test_failing', + 'value' => 'i:8000;', + ]) + ->execute(); + +// Update core.extension. +$extensions = $connection->select('config') + ->fields('config', ['data']) + ->condition('collection', '') + ->condition('name', 'core.extension') + ->execute() + ->fetchField(); +$extensions = unserialize($extensions); +$extensions['module']['update_test_failing'] = 8000; +$connection->update('config') + ->fields([ + 'data' => serialize($extensions), + ]) + ->condition('collection', '') + ->condition('name', 'core.extension') + ->execute(); + diff --git a/core/modules/system/tests/modules/module_test/module_test.post_update.php b/core/modules/system/tests/modules/module_test/module_test.post_update.php new file mode 100644 index 000000000000..22265789628e --- /dev/null +++ b/core/modules/system/tests/modules/module_test/module_test.post_update.php @@ -0,0 +1,7 @@ +get('post_update_test_execution', []); + $execution[] = __FUNCTION__; + \Drupal::state()->set('post_update_test_execution', $execution); +} diff --git a/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.info.yml b/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.info.yml new file mode 100644 index 000000000000..9dcff3aabd2c --- /dev/null +++ b/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.info.yml @@ -0,0 +1,3 @@ +core: 8.x +name: Update test after +type: module diff --git a/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.install b/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.install new file mode 100644 index 000000000000..f754d94a1855 --- /dev/null +++ b/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.install @@ -0,0 +1,7 @@ +get('post_update_test_execution', []); + $execution[] = __FUNCTION__; + \Drupal::state()->set('post_update_test_execution', $execution); + + return 'First update'; +} + +/** + * Second update. + */ +function update_test_postupdate_post_update_second() { + $execution = \Drupal::state()->get('post_update_test_execution', []); + $execution[] = __FUNCTION__; + \Drupal::state()->set('post_update_test_execution', $execution); + + return 'Second update'; +} + +/** + * Test1 update. + */ +function update_test_postupdate_post_update_test1() { + $execution = \Drupal::state()->get('post_update_test_execution', []); + $execution[] = __FUNCTION__; + \Drupal::state()->set('post_update_test_execution', $execution); + + return 'Test1 update'; +} + +/** + * Test0 update. + */ +function update_test_postupdate_post_update_test0() { + $execution = \Drupal::state()->get('post_update_test_execution', []); + $execution[] = __FUNCTION__; + \Drupal::state()->set('post_update_test_execution', $execution); + + return 'Test0 update'; +} diff --git a/core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php b/core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php new file mode 100644 index 000000000000..5de678052d1d --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php @@ -0,0 +1,301 @@ + [ + 'default' => [ + 'modules' => [ + 'module_a' => [ + 'module_a.post_update.php' => $module_a, + 'module_a.info.yml' => $info_a + ], + 'module_b' => [ + 'module_b.post_update.php' => $module_b, + 'module_b.info.yml' => $info_b + ], + ] + ] + ], + ]); + } + + /** + * @covers ::getPendingUpdateFunctions + */ + public function testGetPendingUpdateFunctionsNoExistingUpdates() { + $this->setupBasicModules(); + + $key_value = $this->prophesize(KeyValueStoreInterface::class); + $key_value->get('existing_updates', [])->willReturn([]); + $key_value = $key_value->reveal(); + + $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [ + 'module_a', + 'module_b' + ], $key_value, FALSE); + + $this->assertEquals([ + 'module_a_post_update_a', + 'module_a_post_update_b', + 'module_b_post_update_a' + ], $update_registry->getPendingUpdateFunctions()); + } + + /** + * @covers ::getPendingUpdateFunctions + */ + public function testGetPendingUpdateFunctionsWithLoadedModulesButNotEnabled() { + $this->setupBasicModules(); + + $key_value = $this->prophesize(KeyValueStoreInterface::class); + $key_value->get('existing_updates', [])->willReturn([]); + $key_value = $key_value->reveal(); + + // Preload modules to ensure that ::getAvailableUpdateFunctions filters out + // not enabled modules. + include_once 'vfs://drupal/sites/default/modules/module_a/module_a.post_update.php'; + include_once 'vfs://drupal/sites/default/modules/module_b/module_b.post_update.php'; + + $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [ + 'module_a', + ], $key_value, FALSE); + + $this->assertEquals([ + 'module_a_post_update_a', + 'module_a_post_update_b', + ], $update_registry->getPendingUpdateFunctions()); + } + + /** + * @covers ::getPendingUpdateFunctions + */ + public function testGetPendingUpdateFunctionsExistingUpdates() { + $this->setupBasicModules(); + + $key_value = $this->prophesize(KeyValueStoreInterface::class); + $key_value->get('existing_updates', [])->willReturn(['module_a_post_update_a']); + $key_value = $key_value->reveal(); + + $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [ + 'module_a', + 'module_b' + ], $key_value, FALSE); + + $this->assertEquals(array_values([ + 'module_a_post_update_b', + 'module_b_post_update_a' + ]), array_values($update_registry->getPendingUpdateFunctions())); + + } + + /** + * @covers ::getPendingUpdateInformation + */ + public function testGetPendingUpdateInformation() { + $this->setupBasicModules(); + + $key_value = $this->prophesize(KeyValueStoreInterface::class); + $key_value->get('existing_updates', [])->willReturn([]); + $key_value = $key_value->reveal(); + + $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [ + 'module_a', + 'module_b' + ], $key_value, FALSE); + + $expected = []; + $expected['module_a']['pending']['a'] = 'Module A update A.'; + $expected['module_a']['pending']['b'] = 'Module A update B.'; + $expected['module_a']['start'] = 'a'; + $expected['module_b']['pending']['a'] = 'Module B update A.'; + $expected['module_b']['start'] = 'a'; + + $this->assertEquals($expected, $update_registry->getPendingUpdateInformation()); + } + + /** + * @covers ::getPendingUpdateInformation + */ + public function testGetPendingUpdateInformationWithExistingUpdates() { + $this->setupBasicModules(); + + $key_value = $this->prophesize(KeyValueStoreInterface::class); + $key_value->get('existing_updates', [])->willReturn(['module_a_post_update_a']); + $key_value = $key_value->reveal(); + + $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [ + 'module_a', + 'module_b' + ], $key_value, FALSE); + + $expected = []; + $expected['module_a']['pending']['b'] = 'Module A update B.'; + $expected['module_a']['start'] = 'b'; + $expected['module_b']['pending']['a'] = 'Module B update A.'; + $expected['module_b']['start'] = 'a'; + + $this->assertEquals($expected, $update_registry->getPendingUpdateInformation()); + } + + /** + * @covers ::getModuleUpdateFunctions + */ + public function testGetModuleUpdateFunctions() { + $this->setupBasicModules(); + $key_value = $this->prophesize(KeyValueStoreInterface::class)->reveal(); + + $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [ + 'module_a', + 'module_b' + ], $key_value, FALSE); + + $this->assertEquals(['module_a_post_update_a', 'module_a_post_update_b'], array_values($update_registry->getModuleUpdateFunctions('module_a'))); + $this->assertEquals(['module_b_post_update_a'], array_values($update_registry->getModuleUpdateFunctions('module_b'))); + } + + /** + * @covers ::registerInvokedUpdates + */ + public function testRegisterInvokedUpdatesWithoutExistingUpdates() { + $this->setupBasicModules(); + $key_value = $this->prophesize(KeyValueStoreInterface::class); + $key_value->get('existing_updates', [])->willReturn([]); + $key_value->set('existing_updates', ['module_a_post_update_a'])->willReturn(NULL); + $key_value = $key_value->reveal(); + + $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [ + 'module_a', + 'module_b' + ], $key_value, FALSE); + $update_registry->registerInvokedUpdates(['module_a_post_update_a']); + } + + /** + * @covers ::registerInvokedUpdates + */ + public function testRegisterInvokedUpdatesWithMultiple() { + $this->setupBasicModules(); + $key_value = $this->prophesize(KeyValueStoreInterface::class); + $key_value->get('existing_updates', [])->willReturn([]); + $key_value->set('existing_updates', ['module_a_post_update_a', 'module_a_post_update_b'])->willReturn(NULL); + $key_value = $key_value->reveal(); + + $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [ + 'module_a', + 'module_b' + ], $key_value, FALSE); + $update_registry->registerInvokedUpdates(['module_a_post_update_a', 'module_a_post_update_b']); + } + + /** + * @covers ::registerInvokedUpdates + */ + public function testRegisterInvokedUpdatesWithExistingUpdates() { + $this->setupBasicModules(); + $key_value = $this->prophesize(KeyValueStoreInterface::class); + $key_value->get('existing_updates', [])->willReturn(['module_a_post_update_b']); + $key_value->set('existing_updates', ['module_a_post_update_b', 'module_a_post_update_a'])->willReturn(NULL); + $key_value = $key_value->reveal(); + + $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [ + 'module_a', + 'module_b' + ], $key_value, FALSE); + $update_registry->registerInvokedUpdates(['module_a_post_update_a']); + } + + /** + * @covers ::filterOutInvokedUpdatesByModule + */ + public function testFilterOutInvokedUpdatesByModule() { + $this->setupBasicModules(); + $key_value = $this->prophesize(KeyValueStoreInterface::class); + $key_value->get('existing_updates', [])->willReturn(['module_a_post_update_b', 'module_a_post_update_a', 'module_b_post_update_a']); + $key_value->set('existing_updates', ['module_b_post_update_a'])->willReturn(NULL); + $key_value = $key_value->reveal(); + + $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [ + 'module_a', + 'module_b' + ], $key_value, FALSE); + + $update_registry->filterOutInvokedUpdatesByModule('module_a'); + } + +}