diff --git a/core/includes/install.inc b/core/includes/install.inc index 77e0b70..1f52907 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -979,10 +979,30 @@ function drupal_check_module($module) { // Print any error messages foreach ($requirements as $requirement) { if (isset($requirement['severity']) && $requirement['severity'] == REQUIREMENT_ERROR) { - $message = $requirement['description']; + $message = []; + // If the requirement description is already a render array, add it to + // the message array. + if (is_array($requirement['description'])) { + $message['description'] = $requirement['description']; + } + // Otherwise, add a #markup element for the description string. + else { + $message['description'] = [ + '#markup' => $requirement['description'], + '#weight' => 0, + ]; + } + // If a required value was provided for the item, add it to the error + // message. See the hook_requirements() documentation. if (isset($requirement['value']) && $requirement['value']) { - $message = t('@requirements_message (Currently using @item version @version)', array('@requirements_message' => $requirement['description'], '@item' => $requirement['title'], '@version' => $requirement['value'])); + $message['version'] = [ + '#prefix' => ' (', + '#markup' => t('Currently using @item version @version', array('@item' => $requirement['title'], '@version' => $requirement['value'])), + '#suffix' => ')', + '#weight' => 1, + ]; } + \Drupal::service('renderer')->renderPlain($message); drupal_set_message($message, 'error'); } } diff --git a/core/lib/Drupal/Core/Extension/module.api.php b/core/lib/Drupal/Core/Extension/module.api.php index e30fd87..2f7af03 100644 --- a/core/lib/Drupal/Core/Extension/module.api.php +++ b/core/lib/Drupal/Core/Extension/module.api.php @@ -823,7 +823,8 @@ function hook_updater_info_alter(&$updaters) { * - value: The current value (e.g., version, time, level, etc). During * install phase, this should only be used for version numbers, do not set * it if not applicable. - * - description: The description of the requirement/status. + * - description: The description of the requirement/status. May be either a + * string or a render array. * - severity: The requirement's result/severity level, one of: * - REQUIREMENT_INFO: For info only. * - REQUIREMENT_OK: The requirement is satisfied. diff --git a/core/modules/system/src/Tests/Module/DependencyTest.php b/core/modules/system/src/Tests/Module/DependencyTest.php index e9162a8..05f17cd 100644 --- a/core/modules/system/src/Tests/Module/DependencyTest.php +++ b/core/modules/system/src/Tests/Module/DependencyTest.php @@ -104,6 +104,10 @@ function testEnableRequirementsFailureDependency() { $this->assertModules(array('requirements1_test'), FALSE); $this->assertModules(array('requirements2_test'), FALSE); + // Configure requirements1_test to fail installation. + \Drupal::state()->set('requirements1_test.phase', 'install'); + \Drupal::state()->set('requirements1_test.severity', REQUIREMENT_ERROR); + // Attempt to install both modules at the same time. $edit = array(); $edit['modules[Testing][requirements1_test][enable]'] = 'requirements1_test'; @@ -111,7 +115,7 @@ function testEnableRequirementsFailureDependency() { $this->drupalPostForm('admin/modules', $edit, t('Install')); // Makes sure the modules were NOT installed. - $this->assertText(t('Requirements 1 Test failed requirements'), 'Modules status has been updated.'); + $this->assertText('Requirements 1 string'); $this->assertModules(array('requirements1_test'), FALSE); $this->assertModules(array('requirements2_test'), FALSE); diff --git a/core/modules/system/src/Tests/Module/HookRequirementsTest.php b/core/modules/system/src/Tests/Module/HookRequirementsTest.php index 83fdce9..4369f10 100644 --- a/core/modules/system/src/Tests/Module/HookRequirementsTest.php +++ b/core/modules/system/src/Tests/Module/HookRequirementsTest.php @@ -8,24 +8,226 @@ namespace Drupal\system\Tests\Module; /** - * Attempts enabling a module that fails hook_requirements('install'). + * Tests hook_requirements(). * * @group Module */ class HookRequirementsTest extends ModuleTestBase { + + /** + * The requirement phase. + * + * @var string + */ + protected $phase; + + /** + * The requirement severity. + * + * @var int + */ + protected $severity; + + /** + * Whether to use a render array for the requirement description text. + * + * @var bool + */ + protected $descriptionArray; + + /** + * Whether to use a requirement value. + * + * @var bool + */ + protected $useValue; + /** - * Assert that a module cannot be installed if it fails hook_requirements(). + * Tests the result hook_requirements() during install and runtime phases. + * + * @see requirements1_test_requirements() */ - function testHookRequirementsFailure() { - $this->assertModules(array('requirements1_test'), FALSE); + function testHookRequirements() { + // Test installing the module with all possible combinations of values in + // the hook_requirements() hook. - // Attempt to install the requirements1_test module. - $edit = array(); + // Test all allowed severities. + foreach ([REQUIREMENT_ERROR, REQUIREMENT_WARNING, REQUIREMENT_OK, REQUIREMENT_INFO] as $this->severity) { + // Test all allowed phases. + foreach (['install', 'update', 'runtime'] as $this->phase) { + // Test with a render array description and a string description. + foreach ([TRUE, FALSE] as $this->descriptionArray) { + // Test with and without a 'value' key. + foreach ([TRUE, FALSE] as $this->useValue) { + $this->doInstallTest(); + } + } + } + } + + // Now, configure the requirements hook to allow installation, and install + // the module. + \Drupal::state()->set('requirements1_test.severity', REQUIREMENT_INFO); + $this->container->get('module_installer')->install(['requirements1_test']); + + // Test the status report with all possible combinations of values in the + // hook_requirements() hook. + // Test all allowed severities. + foreach ([REQUIREMENT_ERROR, REQUIREMENT_WARNING, REQUIREMENT_OK, REQUIREMENT_INFO] as $this->severity) { + // Test all allowed phases. + foreach (['install', 'update', 'runtime'] as $this->phase) { + // Test with a render array description and a string description. + foreach ([TRUE, FALSE] as $this->descriptionArray) { + // Test with and without a 'value' key. + foreach ([TRUE, FALSE] as $this->useValue) { + // Test the runtime messages. + $this->doRuntimeTest(); + } + } + } + } + } + + /** + * Tests installing the module with the current hook_requirements() values. + * + * @see requirements1_test_requirements() + */ + protected function doInstallTest() { + // Only install phase requirement errors should fail installation. + if (($this->phase == 'install') && ($this->severity == REQUIREMENT_ERROR)) { + $success = FALSE; + } + else { + $success = TRUE; + } + + // Ensure that the module is not currently installed. + $this->assertModules(['requirements1_test'], FALSE); + + // Configure the module's hook_requirements(). + \Drupal::state()->set('requirements1_test.phase', $this->phase); + \Drupal::state()->set('requirements1_test.severity', $this->severity); + \Drupal::state()->set('requirements1_test.description_array', $this->descriptionArray); + \Drupal::state()->set('requirements1_test.use_value', $this->useValue); + + // Attempt to install the module. + $edit = []; $edit['modules[Testing][requirements1_test][enable]'] = 'requirements1_test'; $this->drupalPostForm('admin/modules', $edit, t('Install')); - // Makes sure the module was NOT installed. - $this->assertText(t('Requirements 1 Test failed requirements'), 'Modules status has been updated.'); - $this->assertModules(array('requirements1_test'), FALSE); + // Assert the expected success or failure. + $this->assertModules(['requirements1_test'], $success); + + // If the installation was successful, no requirements message should be + // displayed. A message is only displayed during the install phase when + // installation fails. See the hook_requirements() documentation. + if ($success) { + $this->assertNoRequirementsMessages(); + + // Uninstall the module to prepare for the next test. + $this->container->get('module_installer')->uninstall(['requirements1_test']); + } + + // Otherwise, if the installation was not successful, the requirements + // error should be displayed. + else { + $this->assertExpectedRequirementsMessages(); + } } + + /** + * Tests the status report with the current hook_requirements() values. + * + * @see requirements1_test_requirements() + */ + protected function doRuntimeTest() { + // Configure the module's hook_requirements(). + \Drupal::state()->set('requirements1_test.phase', $this->phase); + \Drupal::state()->set('requirements1_test.severity', $this->severity); + \Drupal::state()->set('requirements1_test.description_array', $this->descriptionArray); + \Drupal::state()->set('requirements1_test.use_value', $this->useValue); + + // Visit the reports page. + $this->drupalGet('admin/reports/status'); + + // If the runtime phase was used, we can expect messages for any severity. + if ($this->phase == 'runtime') { + $this->assertExpectedRequirementsMessages(); + } + + // For other phase values, none of the messages should be displayed. + else { + $this->assertNoRequirementsMessages(); + } + } + + /** + * Asserts the expected hook messages with the current seettings. + * + * @see requirements1_test_requirements() + */ + protected function assertExpectedRequirementsMessages() { + // @todo Currently, the 'title' is only displayed during the install phase + // if the 'value' is also defined. This may not be intentional. Fix in + // https://www.drupal.org/node/2549803. + if (($this->phase == 'install') && !$this->useValue) { + $this->assertNoText('Requirements 1 title'); + } + else { + // Normally, the requirement title should always be displayed. + $this->assertRaw('Requirements 1 title with markup!'); + } + + // If the requirements description was a render array, check that it was + // rendered correctly. + if ($this->descriptionArray) { + $this->assertRaw('Requirements 1 render array with markup!'); + $this->assertText('Requirements 1 first item'); + $this->assertText('Requirements 1 second item'); + } + // Otherwise, test for the string message. + else { + $this->assertRaw('Requirements 1 string with markup!'); + } + + // The value should be displayed if it was set. + if ($this->useValue) { + $this->assertRaw('Requirements 1 value text with markup!'); + } + else { + $this->assertNoText('Requirements 1 value text'); + } + } + + /** + * Asserts that none of the possible requirements messages are displayed. + * + * @see requirements1_test_requirements() + */ + protected function assertNoRequirementsMessages() { + $this->assertNoText('Requirements 1 title'); + $this->assertNoText('Requirements 1 string'); + $this->assertNoText('Requirements 1 render array'); + $this->assertNoText('Requirements 1 first item'); + $this->assertNoText('Requirements 1 second item'); + $this->assertNoText('Requirements 1 value text'); + } + + /** + * {@inheritoc} + */ + protected function assertText($text, $message = '', $group = 'Other') { + $message = "Phase {$this->phase}, severity {$this->severity}, value {$this->useValue}: $text found."; + parent::assertText($text, $message, $group); + } + + /** + * {@inheritoc} + */ + protected function assertNoText($text, $message = '', $group = 'Other') { + $message = "Phase {$this->phase}, severity {$this->severity}, value {$this->useValue}: $text not found."; + parent::assertNoText($text, $message, $group); + } + } diff --git a/core/modules/system/src/Tests/Module/ModuleTestBase.php b/core/modules/system/src/Tests/Module/ModuleTestBase.php index 1f225bb..c2d457f 100644 --- a/core/modules/system/src/Tests/Module/ModuleTestBase.php +++ b/core/modules/system/src/Tests/Module/ModuleTestBase.php @@ -30,7 +30,7 @@ protected function setUp() { parent::setUp(); - $this->adminUser = $this->drupalCreateUser(array('access administration pages', 'administer modules')); + $this->adminUser = $this->drupalCreateUser(array('access administration pages', 'administer modules', 'administer site configuration')); $this->drupalLogin($this->adminUser); } diff --git a/core/modules/system/tests/modules/requirements1_test/requirements1_test.install b/core/modules/system/tests/modules/requirements1_test/requirements1_test.install index 80220b9..943dcb0 100644 --- a/core/modules/system/tests/modules/requirements1_test/requirements1_test.install +++ b/core/modules/system/tests/modules/requirements1_test/requirements1_test.install @@ -2,18 +2,53 @@ /** * Implements hook_requirements(). + * + * Test modules may use four state variables to control this hook: + * - requirements1_test.phase: Whether to add the message for 'install', + * 'update', or 'runtime' phase. + * - requirements1_test.severity: The value for the 'severity' of the + * requirement, e.g. REQUIREMENT_ERROR + * - requirements1_test.description_array: Boolean indicating whether to use a + * render array for the description message (TRUE) or a plain string (FALSE). + * - requirements1_test.use_value: Whether to use the fantastic 'value' key, + * which has different meanings in different contexts! + * + * @see hook_requirements() + * @see \Drupal\system\Tests\Module\HookRequirementsTest */ function requirements1_test_requirements($phase) { $requirements = array(); - // Always fails requirements. - if ('install' == $phase) { - $requirements['requirements1_test'] = array( - 'title' => t('Requirements 1 Test'), - 'severity' => REQUIREMENT_ERROR, - 'description' => t('Requirements 1 Test failed requirements.'), - ); + // Add the requirement only for the phase set in the test. + if ($phase != \Drupal::state()->get('requirements1_test.phase')) { + return $requirements; } + // Prepare the requirement message. + $requirement = []; + $requirement['title'] = t('Requirements 1 title with markup!'); + + // Use the severity level set in the test. + $requirement['severity'] = \Drupal::state()->get('requirements1_test.severity'); + + // Add a value to the requirement if indicated in the test. + if (\Drupal::state()->get('requirements1_test.use_value')) { + $requirement['value'] = t('Requirements 1 value text with markup!'); + } + + // Use a render array or a string for the description based on the value set + // in the test. + if (\Drupal::state()->get('requirements1_test.description_array')) { + $requirement['description'] = [ + '#theme' => 'item_list', + '#title' => t('Requirements 1 render array with markup!'), + '#items' => ['Requirements 1 first item', 'Requirements 1 second item'], + ]; + } + else { + $requirement['description'] = t('Requirements 1 string with markup!.'); + } + + $requirements['requirements1_test'] = $requirement; return $requirements; }