diff --git a/core/core.services.yml b/core/core.services.yml index 3e2feb6..5fbcecf 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -289,7 +289,19 @@ services: arguments: ['@app.root', '%container.modules%', '@cache.bootstrap'] module_installer: class: Drupal\Core\Extension\ModuleInstaller + tags: + - { name: service_collector, tag: 'module_install.uninstall_validator', call: addUninstallValidator } arguments: ['@app.root', '@module_handler', '@kernel'] + content_uninstall_validator: + class: Drupal\Core\Entity\ContentUninstallValidator + tags: + - { name: module_install.uninstall_validator } + arguments: ['@entity.manager', '@string_translation'] + field_uninstall_validator: + class: Drupal\Core\Field\FieldModuleUninstallValidator + tags: + - { name: module_install.uninstall_validator } + arguments: ['@entity.manager', '@string_translation'] theme_handler: class: Drupal\Core\Extension\ThemeHandler arguments: ['@app.root', '@config.factory', '@module_handler', '@state', '@info_parser', '@logger.channel.default', '@asset.css.collection_optimizer', '@config.installer', '@config.manager', '@router.builder_indicator'] diff --git a/core/lib/Drupal/Core/Entity/ContentUninstallValidator.php b/core/lib/Drupal/Core/Entity/ContentUninstallValidator.php new file mode 100644 index 0000000..cc139e0 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/ContentUninstallValidator.php @@ -0,0 +1,47 @@ +entityManager = $entity_manager; + $this->stringTranslation = $string_translation; + } + + /** + * {@inheritdoc} + */ + public function validate($module) { + $entity_types = $this->entityManager->getDefinitions(); + $reasons = array(); + foreach ($entity_types as $entity_type) { + if ($module == $entity_type->getProvider() && $entity_type instanceof ContentEntityTypeInterface && $this->entityManager->getStorage($entity_type->id())->hasData()) { + $reasons[] = $this->t('There is content for the entity type: @entity_type', array('@entity_type' => $entity_type->getLabel())); + } + } + return $reasons; + } + +} diff --git a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php index f6c7be5..9d12df6 100644 --- a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php +++ b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php @@ -309,6 +309,6 @@ public function getModuleDirectories(); * @return string * Returns the human readable name of the module. */ - public function getName($theme); + public function getName($module); } diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php index c77dd34..c6f3921 100644 --- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php +++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php @@ -42,6 +42,13 @@ class ModuleInstaller implements ModuleInstallerInterface { protected $root; /** + * The uninstall validators. + * + * @var \Drupal\Core\Extension\ModuleUninstallValidatorInterface[] + */ + protected $uninstallValidators; + + /** * Constructs a new ModuleInstaller instance. * * @param string $root @@ -63,6 +70,13 @@ public function __construct($root, ModuleHandlerInterface $module_handler, Drupa /** * {@inheritdoc} */ + public function addUninstallValidator(ModuleUninstallValidatorInterface $uninstall_validator) { + $this->uninstallValidators[] = $uninstall_validator; + } + + /** + * {@inheritdoc} + */ public function install(array $module_list, $enable_dependencies = TRUE) { $extension_config = \Drupal::config('core.extension'); if ($enable_dependencies) { @@ -306,6 +320,15 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) { } } + // Use the validators and throw an exception with the reasons. + if ($reasons = $this->validateUninstall($module_list)) { + foreach ($reasons as $reason) { + $reason_message[] = implode(', ', $reason); + } + throw new ModuleUninstallValidatorException(format_string('The following reasons prevents the modules from being uninstalled: @reasons', array( + '@reasons' => implode(', ', $reason_message), + ))); + } // Set the actual module weights. $module_list = array_map(function ($module) use ($module_data) { return $module_data[$module]->sort; @@ -469,4 +492,23 @@ protected function updateKernel($module_filenames) { $this->moduleHandler = $container->get('module_handler'); } + /** + * {@inheritdoc} + */ + public function validateUninstall(array $module_list) { + $reasons = array(); + foreach ($module_list as $module) { + foreach ($this->uninstallValidators as $validator) { + $validation_reasons = $validator->validate($module); + if (!empty($validation_reasons)) { + if (!isset($reasons[$module])) { + $reasons[$module] = array(); + } + $reasons[$module] = array_merge($reasons[$module], $validation_reasons); + } + } + } + return $reasons; + } + } diff --git a/core/lib/Drupal/Core/Extension/ModuleInstallerInterface.php b/core/lib/Drupal/Core/Extension/ModuleInstallerInterface.php index 246af4b..95cb25e 100644 --- a/core/lib/Drupal/Core/Extension/ModuleInstallerInterface.php +++ b/core/lib/Drupal/Core/Extension/ModuleInstallerInterface.php @@ -23,7 +23,7 @@ * - Invoke hook_install() and add it to the list of installed modules. * - Invoke hook_modules_installed(). * - * @param array $module_list + * @param string[] $module_list * An array of module names. * @param bool $enable_dependencies * (optional) If TRUE, dependencies will automatically be installed in the @@ -42,7 +42,7 @@ public function install(array $module_list, $enable_dependencies = TRUE); /** * Uninstalls a given list of modules. * - * @param array $module_list + * @param string[] $module_list * The modules to uninstall. * @param bool $uninstall_dependents * (optional) If TRUE, dependent modules will automatically be uninstalled @@ -58,5 +58,24 @@ public function install(array $module_list, $enable_dependencies = TRUE); */ public function uninstall(array $module_list, $uninstall_dependents = TRUE); + /** + * Adds module a uninstall validator. + * + * @param \Drupal\Core\Extension\ModuleUninstallValidatorInterface $uninstall_validator + * The uninstall validator to add. + */ + public function addUninstallValidator(ModuleUninstallValidatorInterface $uninstall_validator); + + /** + * Determines whether a list of modules can be uninstalled. + * + * @param string[] $module_list + * An array of module names. + * + * @return string[] + * An array of reasons the module can not be uninstalled, empty if it can. + */ + public function validateUninstall(array $module_list); + } diff --git a/core/lib/Drupal/Core/Extension/ModuleUninstallValidatorException.php b/core/lib/Drupal/Core/Extension/ModuleUninstallValidatorException.php new file mode 100644 index 0000000..c28240e --- /dev/null +++ b/core/lib/Drupal/Core/Extension/ModuleUninstallValidatorException.php @@ -0,0 +1,13 @@ +entityManager = $entity_manager; + $this->stringTranslation = $string_translation; + } + + /** + * {@inheritdoc} + */ + public function validate($module_name) { + $entity_types = $this->entityManager->getDefinitions(); + $reasons = array(); + foreach ($entity_types as $entity_type_id => $entity_type) { + // We skip entity types defined by the module as there may be no content + // for being able to uninstall them anyway. + // See \Drupal\Core\Entity\ContentUninstallValidator. + if ($entity_type->getProvider() != $module_name && $entity_type instanceof ContentEntityTypeInterface) { + foreach ($this->entityManager->getFieldStorageDefinitions($entity_type_id) as $storage_definition) { + if ($storage_definition->getProvider() == $module_name) { + $storage = $this->entityManager->getStorage($entity_type_id); + // If the content is not dynamically fieldable, fallback to checking + // whether the entity has content in general. + $fieldable = $storage instanceof DynamicallyFieldableEntityStorageInterface; + if (($fieldable && $storage->countFieldData($storage_definition, TRUE)) || (!$fieldable && $storage->hasData())) { + $reasons[] = $this->t('There is content for the field @field-name on entity type @entity_type.', array( + '@field-name' => $storage_definition->getName(), + '@entity_type' => $entity_type->getLabel(), + )); + } + } + } + } + } + return $reasons; + } + +} diff --git a/core/modules/config/src/Tests/ConfigImportAllTest.php b/core/modules/config/src/Tests/ConfigImportAllTest.php index b620ad8..a7ce00d 100644 --- a/core/modules/config/src/Tests/ConfigImportAllTest.php +++ b/core/modules/config/src/Tests/ConfigImportAllTest.php @@ -9,6 +9,7 @@ use Drupal\Core\Config\StorageComparer; use Drupal\system\Tests\Module\ModuleTestBase; +use Drupal\shortcut\Entity\Shortcut; /** * Tests the largest configuration import possible with the modules and profiles @@ -83,6 +84,10 @@ public function testInstallUninstall() { $term->delete(); } + // Delete any shortcuts so the shortcut module can be uninstalled. + $shortcuts = Shortcut::loadMultiple(); + entity_delete_multiple('shortcut', array_keys($shortcuts)); + system_list_reset(); $all_modules = system_rebuild_module_data(); diff --git a/core/modules/system/src/Form/ModulesUninstallForm.php b/core/modules/system/src/Form/ModulesUninstallForm.php index 4285956..153e2ca 100644 --- a/core/modules/system/src/Form/ModulesUninstallForm.php +++ b/core/modules/system/src/Form/ModulesUninstallForm.php @@ -8,6 +8,7 @@ namespace Drupal\system\Form; use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Extension\ModuleInstallerInterface; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface; @@ -26,6 +27,13 @@ class ModulesUninstallForm extends FormBase { protected $moduleHandler; /** + * The module installer service. + * + * @var \Drupal\Core\Extension\ModuleInstallerInterface + */ + protected $moduleInstaller; + + /** * The expirable key value store. * * @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface @@ -38,6 +46,7 @@ class ModulesUninstallForm extends FormBase { public static function create(ContainerInterface $container) { return new static( $container->get('module_handler'), + $container->get('module_installer'), $container->get('keyvalue.expirable')->get('modules_uninstall') ); } @@ -47,11 +56,14 @@ public static function create(ContainerInterface $container) { * * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler. + * @param \Drupal\Core\Extension\ModuleInstallerInterface $module_installer + * The module installer. * @param \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $key_value_expirable * The key value expirable factory. */ - public function __construct(ModuleHandlerInterface $module_handler, KeyValueStoreExpirableInterface $key_value_expirable) { + public function __construct(ModuleHandlerInterface $module_handler, ModuleInstallerInterface $module_installer, KeyValueStoreExpirableInterface $key_value_expirable) { $this->moduleHandler = $module_handler; + $this->moduleInstaller = $module_installer; $this->keyValueExpirable = $key_value_expirable; } @@ -69,7 +81,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { // Make sure the install API is available. include_once DRUPAL_ROOT . '/core/includes/install.inc'; - // Get a list of disabled, installed modules. + // Get a list of all available modules. $modules = system_rebuild_module_data(); $uninstallable = array_filter($modules, function ($module) use ($modules) { return empty($modules[$module->getName()]->info['required']) && drupal_get_installed_schema_version($module->getName()) > SCHEMA_UNINSTALLED; @@ -110,9 +122,10 @@ public function buildForm(array $form, FormStateInterface $form_state) { // Sort all modules by their name. uasort($uninstallable, 'system_sort_modules_by_info_name'); + $validation_reasons = $this->moduleInstaller->validateUninstall(array_keys($uninstallable)); $form['uninstall'] = array('#tree' => TRUE); - foreach ($uninstallable as $module) { + foreach ($uninstallable as $module_key => $module) { $name = $module->info['name'] ?: $module->getName(); $form['modules'][$module->getName()]['#module_name'] = $name; $form['modules'][$module->getName()]['name']['#markup'] = $name; @@ -124,6 +137,12 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#title_display' => 'invisible', ); + // If a validator returns reasons not to uninstall a module, + // list the reasons and disable the check box. + if (isset($validation_reasons[$module_key])) { + $form['modules'][$module->getName()]['#validation_reasons'] = $validation_reasons[$module_key]; + $form['uninstall'][$module->getName()]['#disabled'] = TRUE; + } // All modules which depend on this one must be uninstalled first, before // we can allow this module to be uninstalled. (The installation profile // is excluded from this list.) diff --git a/core/modules/system/src/Tests/Extension/ModuleHandlerTest.php b/core/modules/system/src/Tests/Extension/ModuleHandlerTest.php index 83cd5fc..48fbf0e 100644 --- a/core/modules/system/src/Tests/Extension/ModuleHandlerTest.php +++ b/core/modules/system/src/Tests/Extension/ModuleHandlerTest.php @@ -9,7 +9,7 @@ use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\simpletest\KernelTestBase; -use Symfony\Component\HttpFoundation\Response; +use \Drupal\Core\Extension\ModuleUninstallValidatorException; /** * Tests ModuleHandler functionality. @@ -200,6 +200,60 @@ function testUninstallProfileDependency() { } /** + * Tests uninstalling a module that has content. + */ + function testUninstallContentDependency() { + $this->enableModules(array('module_test', 'entity_test', 'text', 'user', 'help')); + $this->assertTrue($this->moduleHandler()->moduleExists('entity_test'), 'Test module is enabled.'); + $this->assertTrue($this->moduleHandler()->moduleExists('module_test'), 'Test module is enabled.'); + + $this->installSchema('user', 'users_data'); + $entity_types = \Drupal::entityManager()->getDefinitions(); + foreach ($entity_types as $entity_type) { + if ('entity_test' == $entity_type->getProvider()) { + $this->installEntitySchema($entity_type->id()); + } + } + + // Create a fake dependency. + // entity_test will depend on help. This way help can not be uninstalled + // when there is test content preventing entity_test from being uninstalled. + \Drupal::state()->set('module_test.dependency', 'dependency'); + drupal_static_reset('system_rebuild_module_data'); + + // Create an entity so that the modules can not be disabled. + $entity = entity_create('entity_test', array('name' => $this->randomString())); + $entity->save(); + + // Uninstalling entity_test is not possible when there is content. + try { + $message = 'ModuleHandler::uninstall() throws ModuleUninstallValidatorException upon uninstalling a module which does not pass validation.'; + $this->moduleInstaller()->uninstall(array('entity_test')); + $this->fail($message); + } + catch (ModuleUninstallValidatorException $e) { + $this->pass(get_class($e) . ': ' . $e->getMessage()); + } + + // Uninstalling help needs entity_test to be un-installable. + try { + $message = 'ModuleHandler::uninstall() throws ModuleUninstallValidatorException upon uninstalling a module which does not pass validation.'; + $this->moduleInstaller()->uninstall(array('help')); + $this->fail($message); + } + catch (ModuleUninstallValidatorException $e) { + $this->pass(get_class($e) . ': ' . $e->getMessage()); + } + + // Deleting the entity. + $entity->delete(); + + $result = $this->moduleInstaller()->uninstall(array('help')); + $this->assertTrue($result, 'ModuleHandler::uninstall() returns TRUE.'); + $this->assertEqual(drupal_get_installed_schema_version('entity_test'), SCHEMA_UNINSTALLED, "entity_test module was uninstalled."); + } + + /** * Tests whether the correct module metadata is returned. */ function testModuleMetaData() { diff --git a/core/modules/system/src/Tests/Field/FieldModuleUninstallValidatorTest.php b/core/modules/system/src/Tests/Field/FieldModuleUninstallValidatorTest.php new file mode 100644 index 0000000..d9feabc --- /dev/null +++ b/core/modules/system/src/Tests/Field/FieldModuleUninstallValidatorTest.php @@ -0,0 +1,141 @@ +installSchema('user', 'users_data'); + $this->entityDefinitionUpdateManager = $this->container->get('entity.definition_update_manager'); + + // Setup some fields for entity_test_extra to create. + $definitions['extra_base_field'] = BaseFieldDefinition::create('string') + ->setName('extra_base_field') + ->setTargetEntityTypeId('entity_test') + ->setTargetBundle('entity_test'); + $this->state->set('entity_test.additional_base_field_definitions', $definitions); + // @todo: Use better field definition classes once there are any. + $definitions['extra_bundle_field'] = FieldStorageDefinition::create('string') + ->setName('extra_bundle_field') + ->setTargetEntityTypeId('entity_test') + ->setTargetBundle('entity_test'); + $this->state->set('entity_test.additional_field_storage_definitions', $definitions); + $this->state->set('entity_test.entity_test.additional_bundle_field_definitions', $definitions); + $this->entityManager->clearCachedDefinitions(); + } + + /** + * Tests uninstall entity_test module with and without content for the field. + */ + public function testUninstallingModule() { + // Test uninstall works fine without content. + $this->assertModuleInstallUninstall('entity_test_extra'); + + // Test uninstalling works fine with content having no field values. + $entity = $this->entityManager->getStorage('entity_test')->create([ + 'name' => $this->randomString(), + ]); + $entity->save(); + $this->assertModuleInstallUninstall('entity_test_extra'); + $entity->delete(); + + // Verify uninstall works fine without content again. + $this->assertModuleInstallUninstall('entity_test_extra'); + // Verify uninstalling entity_test is not possible when there is content for + // the base field. + $this->enableModules(['entity_test_extra']); + $this->entityDefinitionUpdateManager->applyUpdates(); + $entity = $this->entityManager->getStorage('entity_test')->create([ + 'name' => $this->randomString(), + 'extra_base_field' => $this->randomString(), + ]); + $entity->save(); + + try { + $this->getModuleInstaller()->uninstall(array('entity_test_extra')); + $this->fail('Module uninstallation fails as the module provides a base field which has content.'); + } + catch (ModuleUninstallValidatorException $e) { + $this->pass('Module uninstallation fails as the module provides a base field which has content.'); + } + + // Verify uninstalling entity_test is not possible when there is content for + // the bundle field. + $entity->delete(); + $this->assertModuleInstallUninstall('entity_test_extra'); + $this->enableModules(['entity_test_extra']); + $this->entityDefinitionUpdateManager->applyUpdates(); + $entity = $this->entityManager->getStorage('entity_test')->create([ + 'name' => $this->randomString(), + 'extra_bundle_field' => $this->randomString(), + ]); + $entity->save(); + try { + $this->getModuleInstaller()->uninstall(array('entity_test_extra')); + $this->fail('Module uninstallation fails as the module provides a bundle field which has content.'); + } + catch (ModuleUninstallValidatorException $e) { + $this->pass('Module uninstallation fails as the module provides a bundle field which has content.'); + } + } + + /** + * Asserts the given module can be installed and uninstalled. + * + * @param string $module_name + * The module to install and uninstall. + */ + protected function assertModuleInstallUninstall($module_name) { + $this->enableModules([$module_name]); + $this->entityDefinitionUpdateManager->applyUpdates(); + $this->assertTrue($this->getModuleHandler()->moduleExists($module_name), $module_name .' module is enabled.'); + $this->getModuleInstaller()->uninstall([$module_name]); + $this->entityDefinitionUpdateManager->applyUpdates(); + $this->assertFalse($this->getModuleHandler()->moduleExists($module_name), $module_name . ' module is disabled.'); + } + + /** + * Returns the ModuleHandler. + * + * @return \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected function getModuleHandler() { + return $this->container->get('module_handler'); + } + + /** + * Returns the ModuleInstaller. + * + * @return \Drupal\Core\Extension\ModuleInstallerInterface + */ + protected function getModuleInstaller() { + return $this->container->get('module_installer'); + } + +} diff --git a/core/modules/system/src/Tests/Module/UninstallTest.php b/core/modules/system/src/Tests/Module/UninstallTest.php index f48a8aa..aa06bda 100644 --- a/core/modules/system/src/Tests/Module/UninstallTest.php +++ b/core/modules/system/src/Tests/Module/UninstallTest.php @@ -43,9 +43,22 @@ function testUserPermsUninstalled() { function testUninstallPage() { $account = $this->drupalCreateUser(array('administer modules')); $this->drupalLogin($account); + + // Create a node type. + $node_type = entity_create('node_type', array('type' => 'uninstall_blocker')); + $node_type->save(); + // Add a node to prevent node from being uninstalled. + $node = entity_create('node', array('type' => 'uninstall_blocker')); + $node->save(); + $this->drupalGet('admin/modules/uninstall'); $this->assertTitle(t('Uninstall') . ' | Drupal'); + $this->assertText(\Drupal::translation()->translate('The following reasons prevents Node from being uninstalled: There is content for the entity type: Content'), 'Content prevents uninstalling node module.'); + // Delete the node to allow node to be uninstalled. + $node->delete(); + $node_type->delete(); + // Uninstall module_test. $edit = array(); $edit['uninstall[module_test]'] = TRUE; diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc index 2ef1ea2..197767f 100644 --- a/core/modules/system/system.admin.inc +++ b/core/modules/system/system.admin.inc @@ -303,14 +303,16 @@ function theme_system_modules_uninstall($variables) { // Display table. $rows = array(); foreach (Element::children($form['modules']) as $module) { + $disabled_message = ''; + // Add the modules requiring the module in question as a validation reason. if (!empty($form['modules'][$module]['#required_by'])) { - $disabled_message = format_plural(count($form['modules'][$module]['#required_by']), - 'To uninstall @module, the following module must be uninstalled first: @required_modules', - 'To uninstall @module, the following modules must be uninstalled first: @required_modules', - array('@module' => $form['modules'][$module]['#module_name'], '@required_modules' => implode(', ', $form['modules'][$module]['#required_by']))); + $form['modules'][$module]['#validation_reasons'][] = \Drupal::translation()->translate('Required by: @modules', array('@modules' => implode(', ',$form['modules'][$module]['#required_by']))); } - else { - $disabled_message = ''; + if (!empty($form['modules'][$module]['#validation_reasons'])) { + $disabled_message = \Drupal::translation()->formatPlural(count($form['modules'][$module]['#validation_reasons']), + 'The following reason prevents @module from being uninstalled: @reasons', + 'The following reasons prevents @module from being uninstalled: @reasons', + array('@module' => $form['modules'][$module]['#module_name'], '@reasons' => implode('; ', $form['modules'][$module]['#validation_reasons']))); } $rows[] = array( array('data' => drupal_render($form['uninstall'][$module]), 'align' => 'center'), diff --git a/core/modules/system/tests/modules/entity_test_extra/entity_test_extra.info.yml b/core/modules/system/tests/modules/entity_test_extra/entity_test_extra.info.yml new file mode 100644 index 0000000..b5c1cc3 --- /dev/null +++ b/core/modules/system/tests/modules/entity_test_extra/entity_test_extra.info.yml @@ -0,0 +1,8 @@ +name: 'Entity test extra' +type: module +description: 'Provides extra fields for entity test entity types.' +package: Testing +version: VERSION +core: 8.x +dependencies: + - entity_test diff --git a/core/modules/system/tests/modules/entity_test_extra/entity_test_extra.module b/core/modules/system/tests/modules/entity_test_extra/entity_test_extra.module new file mode 100644 index 0000000..d55aab3 --- /dev/null +++ b/core/modules/system/tests/modules/entity_test_extra/entity_test_extra.module @@ -0,0 +1,29 @@ +get($entity_type->id() . '.additional_base_field_definitions', array()); +} + +/** + * Implements hook_entity_field_storage_info(). + */ +function entity_test_extra_entity_field_storage_info(EntityTypeInterface $entity_type) { + return \Drupal::state()->get($entity_type->id() . '.additional_field_storage_definitions', array()); +} + +/** + * Implements hook_entity_bundle_field_info(). + */ +function entity_test_extra_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) { + return \Drupal::state()->get($entity_type->id() . '.' . $bundle . '.additional_bundle_field_definitions', array()); +} diff --git a/core/modules/system/tests/modules/module_test/module_test.module b/core/modules/system/tests/modules/module_test/module_test.module index 60d53ef..92f038b 100644 --- a/core/modules/system/tests/modules/module_test/module_test.module +++ b/core/modules/system/tests/modules/module_test/module_test.module @@ -27,6 +27,10 @@ function module_test_system_info_alter(&$info, Extension $file, $type) { // Make config module depend on help module. $info['dependencies'][] = 'help'; } + elseif ($file->getName() == 'entity_test') { + // Make entity test module depend on help module. + $info['dependencies'][] = 'help'; + } } elseif (\Drupal::state()->get('module_test.dependency') == 'version dependency') { if ($file->getName() == 'color') {