diff --git a/core/includes/update.inc b/core/includes/update.inc
index 4f34dbd4ac..a5ec1f2234 100644
--- a/core/includes/update.inc
+++ b/core/includes/update.inc
@@ -58,8 +58,8 @@ function update_check_incompatibility($name, $type = 'module') {
$file = $themes[$name];
}
if (!isset($file)
- || !isset($file->info['core'])
- || $file->info['core'] != \Drupal::CORE_COMPATIBILITY
+ || !isset($file->info['core_dependency'])
+ || $file->info['core_incompatible']
|| version_compare(phpversion(), $file->info['php']) < 0) {
return TRUE;
}
diff --git a/core/lib/Drupal/Component/Version/DrupalSemver.php b/core/lib/Drupal/Component/Version/DrupalSemver.php
new file mode 100644
index 0000000000..bfce92e17a
--- /dev/null
+++ b/core/lib/Drupal/Component/Version/DrupalSemver.php
@@ -0,0 +1,37 @@
+moduleHandler->alter('system_info', $extension->info, $extension, $this->type);
+
+ // Determine if the extension is compatible with the current version of
+ // Drupal core.
+ $extension->info['core_incompatible'] = !DrupalSemver::satisfies(\Drupal::VERSION, $extension->info['core_dependency']);
}
return $extensions;
diff --git a/core/lib/Drupal/Core/Extension/InfoParserDynamic.php b/core/lib/Drupal/Core/Extension/InfoParserDynamic.php
index 9eb0a56e24..806966f9c5 100644
--- a/core/lib/Drupal/Core/Extension/InfoParserDynamic.php
+++ b/core/lib/Drupal/Core/Extension/InfoParserDynamic.php
@@ -3,6 +3,7 @@
namespace Drupal\Core\Extension;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
+use Drupal\Component\Version\DrupalSemver;
use Drupal\Core\Serialization\Yaml;
/**
@@ -10,6 +11,8 @@
*/
class InfoParserDynamic implements InfoParserInterface {
+ const FIRST_CORE_DEPENDENCY_SUPPORTED_VERSION = '8.7.7';
+
/**
* {@inheritdoc}
*/
@@ -28,6 +31,32 @@ public function parse($filename) {
if (!empty($missing_keys)) {
throw new InfoParserException('Missing required keys (' . implode(', ', $missing_keys) . ') in ' . $filename);
}
+ if (!isset($parsed_info['core']) && !isset($parsed_info['core_dependency'])) {
+ throw new InfoParserException("The 'core' or the 'core_dependency' key must be present in " . $filename);
+ }
+ if (isset($parsed_info['core']) && !preg_match("/^\d\.x$/", $parsed_info['core'])) {
+ throw new InfoParserException("Invalid 'core' value \"{$parsed_info['core']}\" in " . $filename);
+ }
+ if (isset($parsed_info['core_dependency'])) {
+ $supports_pre_core_dependency_version = $this->isConstraintSatisfiedByPreCoreDependencyCoreVersion($parsed_info['core_dependency']);
+ // If the 'core_dependency' constraint does not satisfy any Drupal 8
+ // versions before 8.7.7 then 'core' cannot be set or it will
+ // effectively support all versions of Drupal 8 because
+ // 'core_dependency' will be ignored in previous versions.
+ if (!$supports_pre_core_dependency_version && isset($parsed_info['core'])) {
+ throw new InfoParserException("The 'core_dependency' constraint ({$parsed_info['core_dependency']}) requires the 'core' not be set in " . $filename);
+ }
+ // 'core_dependency' can not be used to specify Drupal 8 versions before
+ // 8.7.7 because these versions do not use the 'core_dependency' key.
+ // Do not throw the exception if the constraint also is satisfied by
+ // 8.0.0-alpha1 to allow constraints such as '^8' or '^8 || ^9'.
+ if ($supports_pre_core_dependency_version && !DrupalSemver::satisfies('8.0.0-alpha1', $parsed_info['core_dependency'])) {
+ throw new InfoParserException("The 'core_dependency' can not be used to specify compatibility specific version before " . static::FIRST_CORE_DEPENDENCY_SUPPORTED_VERSION . " in $filename");
+ }
+ }
+ else {
+ $parsed_info['core_dependency'] = $parsed_info['core'];
+ }
if (isset($parsed_info['version']) && $parsed_info['version'] === 'VERSION') {
$parsed_info['version'] = \Drupal::VERSION;
}
@@ -60,7 +89,47 @@ public function parse($filename) {
* An array of required keys.
*/
protected function getRequiredKeys() {
- return ['type', 'core', 'name'];
+ return ['type', 'name'];
+ }
+
+ /**
+ * Determines if a constraint is satisfied by core without 'core_dependency'.
+ *
+ * @param string $constraint
+ * A core semantic version constraint.
+ *
+ * @return bool
+ * TRUE if the constraint is satisfied by a core version that does not
+ * support the 'core_dependency' key in info.yml files.
+ */
+ protected function isConstraintSatisfiedByPreCoreDependencyCoreVersion($constraint) {
+ static $evaluated_constraints = [];
+ if (!isset($evaluated_constraints[$constraint])) {
+ foreach (range(0, 7) as $minor) {
+ foreach (range(0, 20) as $patch) {
+ $minor_version = "8.$minor.$patch";
+ if ($minor_version === static::FIRST_CORE_DEPENDENCY_SUPPORTED_VERSION) {
+ $evaluated_constraints[$constraint] = FALSE;
+ return $evaluated_constraints[$constraint];
+ }
+ if (DrupalSemver::satisfies($minor_version, $constraint)) {
+ $evaluated_constraints[$constraint] = TRUE;
+ return $evaluated_constraints[$constraint];
+ }
+ if ($patch === 0) {
+ foreach (['alpha1', 'beta1', 'rc1'] as $suffix) {
+ $pre_release_version = "$minor_version-$suffix";
+ if (DrupalSemver::satisfies($minor_version, $pre_release_version)) {
+ $evaluated_constraints[$constraint] = TRUE;
+ return $evaluated_constraints[$constraint];
+ }
+ }
+ }
+ }
+ }
+ }
+ $evaluated_constraints[$constraint] = FALSE;
+ return $evaluated_constraints[$constraint];
}
}
diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
index 5246946e09..e0327fb769 100644
--- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php
+++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
@@ -81,11 +81,17 @@ public function addUninstallValidator(ModuleUninstallValidatorInterface $uninsta
*/
public function install(array $module_list, $enable_dependencies = TRUE) {
$extension_config = \Drupal::configFactory()->getEditable('core.extension');
+ // Get all module data so we can find dependencies and sort and find the
+ // core requirements. The module list needs to be reset so that it can
+ // re-scan and include any new modules that may have been added directly
+ // into the filesystem.
+ $module_data = \Drupal::service('extension.list.module')->reset()->getList();
+ foreach ($module_list as $module) {
+ if (!empty($module_data[$module]->info['core_incompatible'])) {
+ throw new MissingDependencyException("Unable to install modules: module '$module' is incompatible with this version of Drupal core.");
+ }
+ }
if ($enable_dependencies) {
- // Get all module data so we can find dependencies and sort.
- // The module list needs to be reset so that it can re-scan and include
- // any new modules that may have been added directly into the filesystem.
- $module_data = \Drupal::service('extension.list.module')->reset()->getList();
$module_list = $module_list ? array_combine($module_list, $module_list) : [];
if ($missing_modules = array_diff_key($module_list, $module_data)) {
// One or more of the given modules doesn't exist.
@@ -110,6 +116,9 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
// Skip already installed modules.
if (!isset($module_list[$dependency]) && !isset($installed_modules[$dependency])) {
+ if ($module_data[$dependency]->info['core_incompatible']) {
+ throw new MissingDependencyException("Unable to install modules: module '$module'. Its dependency module '$dependency' is incompatible with this version of Drupal core.");
+ }
$module_list[$dependency] = $dependency;
}
}
diff --git a/core/modules/system/src/Controller/SystemController.php b/core/modules/system/src/Controller/SystemController.php
index 72c2e8adea..ca263a7d13 100644
--- a/core/modules/system/src/Controller/SystemController.php
+++ b/core/modules/system/src/Controller/SystemController.php
@@ -223,7 +223,7 @@ public function themesPage() {
if (empty($theme->status)) {
// Ensure this theme is compatible with this version of core.
- $theme->incompatible_core = !isset($theme->info['core']) || ($theme->info['core'] != \DRUPAL::CORE_COMPATIBILITY);
+ $theme->incompatible_core = $theme->info['core_incompatible'];
// Require the 'content' region to make sure the main page
// content has a common place in all themes.
$theme->incompatible_region = !isset($theme->info['regions']['content']);
diff --git a/core/modules/system/src/Form/ModulesListForm.php b/core/modules/system/src/Form/ModulesListForm.php
index 258930eede..bf73eeccde 100644
--- a/core/modules/system/src/Form/ModulesListForm.php
+++ b/core/modules/system/src/Form/ModulesListForm.php
@@ -294,10 +294,14 @@ protected function buildRow(array $modules, Extension $module, $distribution) {
$reasons = [];
// Check the core compatibility.
- if ($module->info['core'] != \Drupal::CORE_COMPATIBILITY) {
+ if ($module->info['core_incompatible']) {
$compatible = FALSE;
$reasons[] = $this->t('This version is not compatible with Drupal @core_version and should be replaced.', [
- '@core_version' => \Drupal::CORE_COMPATIBILITY,
+ '@core_version' => \Drupal::VERSION,
+ ]);
+ $row['#requires']['core'] = $this->t('Drupal Core (@core_requirement) (incompatible with version @core_version)', [
+ '@core_requirement' => $module->info['core_dependency'],
+ '@core_version' => \Drupal::VERSION,
]);
}
@@ -341,7 +345,7 @@ protected function buildRow(array $modules, Extension $module, $distribution) {
}
// Disable the checkbox if the dependency is incompatible with this
// version of Drupal core.
- elseif ($modules[$dependency]->info['core'] != \Drupal::CORE_COMPATIBILITY) {
+ elseif ($modules[$dependency]->info['core_incompatible']) {
$row['#requires'][$dependency] = $this->t('@module (incompatible with this version of Drupal core)', [
'@module' => $name,
]);
diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc
index d228917c41..96d8a65608 100644
--- a/core/modules/system/system.admin.inc
+++ b/core/modules/system/system.admin.inc
@@ -294,7 +294,7 @@ function template_preprocess_system_themes_page(&$variables) {
// Make sure to provide feedback on compatibility.
$current_theme['incompatible'] = '';
if (!empty($theme->incompatible_core)) {
- $current_theme['incompatible'] = t("This theme is not compatible with Drupal @core_version. Check that the .info.yml file contains the correct 'core' value.", ['@core_version' => \Drupal::CORE_COMPATIBILITY]);
+ $current_theme['incompatible'] = t("This theme is not compatible with Drupal @core_version. Check that the .info.yml file contains a compatible 'core' or 'core_dependency' value.", ['@core_version' => \Drupal::VERSION]);
}
elseif (!empty($theme->incompatible_region)) {
$current_theme['incompatible'] = t("This theme is missing a 'content' region.");
diff --git a/core/modules/system/tests/fixtures/update/drupal-8.update-test-semver-update-n-enabled.php b/core/modules/system/tests/fixtures/update/drupal-8.update-test-semver-update-n-enabled.php
new file mode 100644
index 0000000000..285b42c1ef
--- /dev/null
+++ b/core/modules/system/tests/fixtures/update/drupal-8.update-test-semver-update-n-enabled.php
@@ -0,0 +1,38 @@
+merge('key_value')
+ ->condition('collection', 'system.schema')
+ ->condition('name', 'update_test_semver_update_n')
+ ->fields([
+ 'collection' => 'system.schema',
+ 'name' => 'update_test_semver_update_n',
+ '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_semver_update_n'] = 8000;
+$connection->update('config')
+ ->fields([
+ 'data' => serialize($extensions),
+ ])
+ ->condition('collection', '')
+ ->condition('name', 'core.extension')
+ ->execute();
diff --git a/core/modules/system/tests/modules/system_core_semver_test/system_core_semver_test.info.yml b/core/modules/system/tests/modules/system_core_semver_test/system_core_semver_test.info.yml
new file mode 100644
index 0000000000..5ca06ba0af
--- /dev/null
+++ b/core/modules/system/tests/modules/system_core_semver_test/system_core_semver_test.info.yml
@@ -0,0 +1,6 @@
+name: 'System core ^8 version test'
+type: module
+description: 'Support module for testing core using semver.'
+package: Testing
+version: 1.0.0
+core_dependency: ^8
diff --git a/core/modules/system/tests/modules/system_incompatible_core_version_test_1x/system_incompatible_core_version_test_1x.info.yml b/core/modules/system/tests/modules/system_incompatible_core_version_test_1x/system_incompatible_core_version_test_1x.info.yml
new file mode 100644
index 0000000000..f79d71dff8
--- /dev/null
+++ b/core/modules/system/tests/modules/system_incompatible_core_version_test_1x/system_incompatible_core_version_test_1x.info.yml
@@ -0,0 +1,6 @@
+name: 'System incompatible core 1.x version test'
+type: module
+description: 'Support module for testing system core incompatibility.'
+package: Testing
+version: 1.0.0
+core: 1.x
diff --git a/core/modules/system/tests/modules/system_test/system_test.module b/core/modules/system/tests/modules/system_test/system_test.module
index affc20ea42..b14262802c 100644
--- a/core/modules/system/tests/modules/system_test/system_test.module
+++ b/core/modules/system/tests/modules/system_test/system_test.module
@@ -62,6 +62,10 @@ function system_test_system_info_alter(&$info, Extension $file, $type) {
}
}
+ if (($core_requirement = \Drupal::state()->get('dependency_test.core_version_requirement')) && $file->getName() === 'common_test') {
+ $info['core_dependency'] = $core_requirement;
+ }
+
// Make the system_dependencies_test visible by default.
if ($file->getName() == 'system_dependencies_test') {
$info['hidden'] = FALSE;
@@ -71,6 +75,7 @@ function system_test_system_info_alter(&$info, Extension $file, $type) {
'system_incompatible_core_version_dependencies_test',
'system_incompatible_module_version_test',
'system_incompatible_core_version_test',
+ 'system_incompatible_core_version_test_1x',
])) {
$info['hidden'] = FALSE;
}
diff --git a/core/modules/system/tests/modules/update_test_semver_update_n/update_test_semver_update_n.info.yml b/core/modules/system/tests/modules/update_test_semver_update_n/update_test_semver_update_n.info.yml
new file mode 100644
index 0000000000..71da4cbcd0
--- /dev/null
+++ b/core/modules/system/tests/modules/update_test_semver_update_n/update_test_semver_update_n.info.yml
@@ -0,0 +1,6 @@
+name: 'Update test hook_update_n semver'
+type: module
+description: 'Support module for update testing with core semver value.'
+package: Testing
+version: VERSION
+core_dependency: ^8
diff --git a/core/modules/system/tests/modules/update_test_semver_update_n/update_test_semver_update_n.install b/core/modules/system/tests/modules/update_test_semver_update_n/update_test_semver_update_n.install
new file mode 100644
index 0000000000..d00ff0cfe5
--- /dev/null
+++ b/core/modules/system/tests/modules/update_test_semver_update_n/update_test_semver_update_n.install
@@ -0,0 +1,13 @@
+set('update_test_semver_update_n_update_8001', 'Yes, I was run. Thanks for testing!');
+}
diff --git a/core/modules/system/tests/src/Functional/Form/ModulesListFormWebTest.php b/core/modules/system/tests/src/Functional/Form/ModulesListFormWebTest.php
index 52e4160cda..c524eebb51 100644
--- a/core/modules/system/tests/src/Functional/Form/ModulesListFormWebTest.php
+++ b/core/modules/system/tests/src/Functional/Form/ModulesListFormWebTest.php
@@ -70,7 +70,7 @@ public function testModulesListFormWithInvalidInfoFile() {
// Confirm that the error message is shown.
$this->assertSession()
- ->pageTextContains('Modules could not be listed due to an error: Missing required keys (core) in ' . $path . '/broken.info.yml');
+ ->pageTextContains("The 'core' or the 'core_dependency' key must be present in " . $path . '/broken.info.yml');
// Check that the module filter text box is available.
$this->assertTrue($this->xpath('//input[@name="text"]'));
diff --git a/core/modules/system/tests/src/Functional/Module/DependencyTest.php b/core/modules/system/tests/src/Functional/Module/DependencyTest.php
index 2e0e60923c..cb57205740 100644
--- a/core/modules/system/tests/src/Functional/Module/DependencyTest.php
+++ b/core/modules/system/tests/src/Functional/Module/DependencyTest.php
@@ -102,6 +102,50 @@ public function testIncompatiblePhpVersionDependency() {
$this->assert(count($checkbox) == 1, 'Checkbox for the module is disabled.');
}
+ /**
+ * Tests enabling modules with different core version specifications.
+ */
+ public function testCoreVersionDependency() {
+ $assert_session = $this->assertSession();
+ list($major, $minor) = explode('.', \Drupal::VERSION);
+
+ $next_minor = $minor + 1;
+ $next_major = $major + 1;
+
+ // Test the next minor release.
+ \Drupal::state()->set('dependency_test.core_version_requirement', "~$major.$next_minor");
+ $this->drupalGet('admin/modules');
+ $assert_session->fieldDisabled('modules[system_incompatible_core_version_test_1x][enable]');
+ $assert_session->fieldDisabled('modules[common_test][enable]');
+
+ // Test either current major or the next one.
+ \Drupal::state()->set('dependency_test.core_version_requirement', "^$major || ^$next_major");
+ $this->drupalGet('admin/modules');
+ $assert_session->fieldEnabled('modules[common_test][enable]');
+
+ // Test either a previous major or the next one.
+ \Drupal::state()->set('dependency_test.core_version_requirement', "^1 || ^$next_major");
+ $this->drupalGet('admin/modules');
+ $assert_session->fieldDisabled('modules[common_test][enable]');
+
+ // Test an invalid major.
+ \Drupal::state()->set('dependency_test.core_version_requirement', 'this-string-is-invalid');
+ $this->drupalGet('admin/modules');
+ $assert_session->fieldDisabled('modules[common_test][enable]');
+
+ // Test the current minor.
+ \Drupal::state()->set('dependency_test.core_version_requirement', "~$major.$minor");
+ $this->drupalGet('admin/modules');
+ $assert_session->fieldEnabled('modules[common_test][enable]');
+ $assert_session->fieldEnabled('modules[system_core_semver_test][enable]');
+
+ // Ensure the modules can actually be installed.
+ $edit['modules[common_test][enable]'] = 'common_test';
+ $edit['modules[system_core_semver_test][enable]'] = 'system_core_semver_test';
+ $this->drupalPostForm('admin/modules', $edit, t('Install'));
+ $this->assertModules(['common_test', 'system_core_semver_test'], TRUE);
+ }
+
/**
* Tests enabling a module that depends on a module which fails hook_requirements().
*/
diff --git a/core/modules/system/tests/src/Functional/System/ThemeTest.php b/core/modules/system/tests/src/Functional/System/ThemeTest.php
index a107459ff6..9d028b49d8 100644
--- a/core/modules/system/tests/src/Functional/System/ThemeTest.php
+++ b/core/modules/system/tests/src/Functional/System/ThemeTest.php
@@ -367,8 +367,11 @@ public function testInvalidTheme() {
$this->assertText(t('This theme requires the base theme @base_theme to operate correctly.', ['@base_theme' => 'not_real_test_basetheme']));
$this->assertText(t('This theme requires the base theme @base_theme to operate correctly.', ['@base_theme' => 'test_invalid_basetheme']));
$this->assertText(t('This theme requires the theme engine @theme_engine to operate correctly.', ['@theme_engine' => 'not_real_engine']));
- // Check for the error text of a theme with the wrong core version.
- $this->assertText("This theme is not compatible with Drupal 8.x. Check that the .info.yml file contains the correct 'core' value.");
+ // Check for the error text of a theme with the wrong core version
+ // using 7.x and ^7.
+ $incompatible_core_message = 'This theme is not compatible with Drupal ' . \Drupal::VERSION . ". Check that the .info.yml file contains a compatible 'core' or 'core_dependency' value.";
+ $this->assertThemeIncompatibleText('Theme test with invalid core version', $incompatible_core_message);
+ $this->assertThemeIncompatibleText('Theme test with invalid semver core version', $incompatible_core_message);
// Check for the error text of a theme without a content region.
$this->assertText("This theme is missing a 'content' region.");
}
@@ -437,24 +440,28 @@ public function testUninstallingThemes() {
* Tests installing a theme and setting it as default.
*/
public function testInstallAndSetAsDefault() {
- $this->drupalGet('admin/appearance');
- // Bartik is uninstalled in the test profile and has the third "Install and
- // set as default" link.
- $this->clickLink(t('Install and set as default'), 2);
- // Test the confirmation message.
- $this->assertText('Bartik is now the default theme.');
- // Make sure Bartik is now set as the default theme in config.
- $this->assertEqual($this->config('system.theme')->get('default'), 'bartik');
-
- // This checks for a regression. See https://www.drupal.org/node/2498691.
- $this->assertNoText('The bartik theme was not found.');
-
- $themes = \Drupal::service('theme_handler')->rebuildThemeData();
- $version = $themes['bartik']->info['version'];
-
- // Confirm Bartik is indicated as the default theme.
- $out = $this->getSession()->getPage()->getContent();
- $this->assertTrue((bool) preg_match('/Bartik ' . preg_quote($version) . '\s{2,}\(default theme\)/', $out));
+ $themes = [
+ 'bartik' => 'Bartik',
+ 'test_core_semver' => 'Theme test with semver core version',
+ ];
+ foreach ($themes as $theme_machine_name => $theme_name) {
+ $this->drupalGet('admin/appearance');
+ $this->getSession()->getPage()->findLink("Install $theme_name as default theme")->click();
+ // Test the confirmation message.
+ $this->assertText("$theme_name is now the default theme.");
+ // Make sure the theme is now set as the default theme in config.
+ $this->assertEqual($this->config('system.theme')->get('default'), $theme_machine_name);
+
+ // This checks for a regression. See https://www.drupal.org/node/2498691.
+ $this->assertNoText("The $theme_machine_name theme was not found.");
+
+ $themes = \Drupal::service('theme_handler')->rebuildThemeData();
+ $version = $themes[$theme_machine_name]->info['version'];
+
+ // Confirm the theme is indicated as the default theme.
+ $out = $this->getSession()->getPage()->getContent();
+ $this->assertTrue((bool) preg_match("/$theme_name " . preg_quote($version) . '\s{2,}\(default theme\)/', $out));
+ }
}
/**
@@ -470,4 +477,16 @@ public function testThemeSettingsNoLogoNoFavicon() {
$this->assertText('The configuration options have been saved.');
}
+ /**
+ * Asserts that expected incompatibility text is displayed for a theme.
+ *
+ * @param string $theme_name
+ * Theme name to select element on page. This can be a partial name.
+ * @param $expected_text
+ * The expected incompatibility text.
+ */
+ private function assertThemeIncompatibleText($theme_name, $expected_text) {
+ $this->assertSession()->elementExists('css', ".theme-info:contains(\"$theme_name\") .incompatible:contains(\"$expected_text\")");
+ }
+
}
diff --git a/core/modules/system/tests/themes/test_core_semver/test_core_semver.info.yml b/core/modules/system/tests/themes/test_core_semver/test_core_semver.info.yml
new file mode 100644
index 0000000000..6802118b78
--- /dev/null
+++ b/core/modules/system/tests/themes/test_core_semver/test_core_semver.info.yml
@@ -0,0 +1,5 @@
+name: 'Theme test with semver core version'
+type: theme
+description: 'Test theme which has semver core version.'
+version: VERSION
+core_dependency: ^8 || ^9
diff --git a/core/modules/system/tests/themes/test_invalid_core_semver/test_invalid_core_semver.info.yml b/core/modules/system/tests/themes/test_invalid_core_semver/test_invalid_core_semver.info.yml
new file mode 100644
index 0000000000..28300f9c4c
--- /dev/null
+++ b/core/modules/system/tests/themes/test_invalid_core_semver/test_invalid_core_semver.info.yml
@@ -0,0 +1,5 @@
+name: 'Theme test with invalid semver core version'
+type: theme
+description: 'Test theme which has an invalid semver core version.'
+version: VERSION
+core_dependency: ^7
diff --git a/core/modules/update/update.module b/core/modules/update/update.module
index 1327944481..4b7613d49a 100644
--- a/core/modules/update/update.module
+++ b/core/modules/update/update.module
@@ -699,7 +699,7 @@ function update_verify_update_archive($project, $archive_file, $directory) {
$info = \Drupal::service('info_parser')->parse($file->uri);
// If the module or theme is incompatible with Drupal core, set an error.
- if (empty($info['core']) || $info['core'] != \Drupal::CORE_COMPATIBILITY) {
+ if ($info['core_incompatible']) {
$incompatible[] = !empty($info['name']) ? $info['name'] : t('Unknown');
}
else {
@@ -717,7 +717,7 @@ function update_verify_update_archive($project, $archive_file, $directory) {
'%archive_file contains a version of %names that is not compatible with Drupal @version.',
'%archive_file contains versions of modules or themes that are not compatible with Drupal @version: %names',
[
- '@version' => \Drupal::CORE_COMPATIBILITY,
+ '@version' => \Drupal::VERSION,
'%archive_file' => $file_system->basename($archive_file),
'%names' => implode(', ', $incompatible),
]
diff --git a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php
index 942cb886ad..504eb96ab9 100644
--- a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php
+++ b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php
@@ -26,6 +26,7 @@ protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../modules/system/tests/fixtures/update/drupal-8.bare.standard.php.gz',
__DIR__ . '/../../../../modules/system/tests/fixtures/update/drupal-8.update-test-schema-enabled.php',
+ __DIR__ . '/../../../../modules/system/tests/fixtures/update/drupal-8.update-test-semver-update-n-enabled.php',
];
}
@@ -99,8 +100,11 @@ public function testUpdateHookN() {
// Ensure schema has changed.
$this->assertEqual(drupal_get_installed_schema_version('update_test_schema', TRUE), 8001);
+ $this->assertEqual(drupal_get_installed_schema_version('update_test_semver_update_n', TRUE), 8001);
// Ensure the index was added for column a.
$this->assertTrue($connection->schema()->indexExists('update_test_schema_table', 'test'), 'Version 8001 of the update_test_schema module is installed.');
+ // Ensure update_test_semver_update_n_update_8001 was run.
+ $this->assertEquals(\Drupal::state()->get('update_test_semver_update_n_update_8001'), 'Yes, I was run. Thanks for testing!');
}
/**
diff --git a/core/tests/Drupal/KernelTests/Core/Extension/ModuleInstallerTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ModuleInstallerTest.php
index ff87c32760..a0bd2c9002 100644
--- a/core/tests/Drupal/KernelTests/Core/Extension/ModuleInstallerTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Extension/ModuleInstallerTest.php
@@ -3,6 +3,7 @@
namespace Drupal\KernelTests\Core\Extension;
use Drupal\Core\Database\Database;
+use Drupal\Core\Extension\MissingDependencyException;
use Drupal\KernelTests\KernelTestBase;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
@@ -85,4 +86,43 @@ public function testKernelRebuildDuringHookInstall() {
$this->assertTrue($module_installer->install(['module_test']));
}
+ /**
+ * Tests install with a module with an invalid core version constraint.
+ *
+ * @dataProvider providerTestInvalidCoreInstall
+ * @covers ::install
+ */
+ public function testInvalidCoreInstall($install_dependencies) {
+ $this->expectException(MissingDependencyException::class);
+ $this->expectExceptionMessage("Unable to install modules: module 'system_incompatible_core_version_test_1x' is incompatible with this version of Drupal core.");
+ $this->container->get('module_installer')->install(['system_incompatible_core_version_test_1x'], $install_dependencies);
+ }
+
+ /**
+ * Tests install with a dependency with an invalid core version constraint.
+ *
+ * @covers ::install
+ */
+ public function testDependencyInvalidCoreInstall() {
+ $this->expectException(MissingDependencyException::class);
+ $this->expectExceptionMessage("Unable to install modules: module 'system_incompatible_core_version_dependencies_test'. Its dependency module 'system_incompatible_core_version_test' is incompatible with this version of Drupal core.");
+ $this->container->get('module_installer')->install(['system_incompatible_core_version_dependencies_test']);
+ }
+
+ /**
+ * Tests no dependencies install with a dependency with invalid core.
+ *
+ * @covers ::install
+ */
+ public function testDependencyInvalidCoreInstallNoDependencies() {
+ $this->assertTrue($this->container->get('module_installer')->install(['system_incompatible_core_version_dependencies_test'], FALSE));
+ }
+
+ public function providerTestInvalidCoreInstall() {
+ return [
+ 'no dependencies' => [FALSE],
+ 'install_dependencies' => [TRUE],
+ ];
+ }
+
}
diff --git a/core/tests/Drupal/Tests/Component/Version/DrupalSemverTest.php b/core/tests/Drupal/Tests/Component/Version/DrupalSemverTest.php
new file mode 100644
index 0000000000..44285eaba6
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Version/DrupalSemverTest.php
@@ -0,0 +1,57 @@
+assertSame($result, DrupalSemver::satisfies($version, $constraints));
+ }
+
+ public function providerSatisfies() {
+ $cases = [
+ ['8.8.0-dev', '8.x', TRUE],
+ ['8.8.0', '8.x', TRUE],
+ ['8.9.9', '8.x', TRUE],
+ ['8.0.0', '8.x', TRUE],
+ ['9.0.0', '8.x', FALSE],
+ ['9.1.0', '8.x', FALSE],
+ ['8.8.0', '~8', TRUE],
+ ['8.8.0', '^8', TRUE],
+ ['8.7.0', '^8.7.6', FALSE],
+ ['8.7.6', '^8.7.6', TRUE],
+ ['8.7.8', '^8.7.6', TRUE],
+ ['9.0.0', '^8.7.6', FALSE],
+ ['8.0.0', '^8 || ^9', TRUE],
+ ['9.1.1', '^8 || ^9', TRUE],
+ ['9.1.1', '^8.7.6 || ^9', TRUE],
+ ['8.7.8', '^8.7.6 || ^9', TRUE],
+ ['8.6.8', '^8.7.6 || ^9', FALSE],
+ ['8.6.8', '^9', FALSE],
+ ['9.1.1', '^9', TRUE],
+ ['9.0.0', '9.x', TRUE],
+ ['8.8.0', '9.x', FALSE],
+ ['8.8.0', '7.x', FALSE],
+ ['a-super-nonsense-string-will-not-throw-an-exception-but-also-will-not-work', '9.x', FALSE],
+ ['a-super-nonsense-string-will-not-throw-an-exception-but-also-will-not-work', '7.x', FALSE],
+ ['a-super-nonsense-string-will-not-throw-an-exception-but-also-will-not-work', '8.x', FALSE],
+ ];
+ $tests = [];
+ foreach ($cases as $case) {
+ $tests[$case[0] . ":" . $case[1]] = $case;
+ }
+ return $tests;
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Extension/InfoParserUnitTest.php b/core/tests/Drupal/Tests/Core/Extension/InfoParserUnitTest.php
index ee8a45e900..19746f1da6 100644
--- a/core/tests/Drupal/Tests/Core/Extension/InfoParserUnitTest.php
+++ b/core/tests/Drupal/Tests/Core/Extension/InfoParserUnitTest.php
@@ -98,7 +98,154 @@ public function testInfoParserMissingKeys() {
]);
$filename = vfsStream::url('modules/fixtures/missing_keys.info.txt');
$this->expectException('\Drupal\Core\Extension\InfoParserException');
- $this->expectExceptionMessage('Missing required keys (type, core, name) in vfs://modules/fixtures/missing_keys.info.txt');
+ $this->expectExceptionMessage('Missing required keys (type, name) in vfs://modules/fixtures/missing_keys.info.txt');
+ $this->infoParser->parse($filename);
+ }
+
+ /**
+ * Tests that missing 'core' and 'core_dependency' keys are detected.
+ *
+ * @covers ::parse
+ */
+ public function testMissingCoreCoreDependency() {
+ $missing_core_and_core_dependency = << [
+ 'missing_core_and_core_dependency.info.txt' => $missing_core_and_core_dependency,
+ ],
+ ]);
+ $filename = vfsStream::url('modules/fixtures/missing_core_and_core_dependency.info.txt');
+ $this->expectException('\Drupal\Core\Extension\InfoParserException');
+ $this->expectExceptionMessage("The 'core' or the 'core_dependency' key must be present in vfs://modules/fixtures/missing_core_and_core_dependency.info.txt");
+ $this->infoParser->parse($filename);
+ }
+
+ /**
+ * Tests that 'core_dependency: ^8.8' is valid with no 'core' key.
+ *
+ * @covers ::parse
+ */
+ public function testCoreDependency88() {
+ $core_dependency = << [
+ 'core_dependency.info.txt' => $core_dependency,
+ ],
+ ]);
+ $filename = vfsStream::url('modules/fixtures/core_dependency.info.txt');
+ $info_values = $this->infoParser->parse($filename);
+ $this->assertSame($info_values['core_dependency'], '^8.8');
+ }
+
+ /**
+ * Tests that 'core_dependency: ^8.8' is invalid with a 'core' key.
+ *
+ * @covers ::parse
+ */
+ public function testCoreCoreDependency88() {
+ $core_and_core_dependency_88 = << [
+ 'core_and_core_dependency_88.info.txt' => $core_and_core_dependency_88,
+ ],
+ ]);
+ $filename = vfsStream::url('modules/fixtures/core_and_core_dependency_88.info.txt');
+ $this->expectException('\Drupal\Core\Extension\InfoParserException');
+ $this->expectExceptionMessage("The 'core_dependency' constraint (^8.8) requires the 'core' not be set in vfs://modules/fixtures/core_and_core_dependency_88.info.txt");
+ $this->infoParser->parse($filename);
+ }
+
+ /**
+ * Tests that 'core_dependency: ^8.8' is invalid with a 'core' key.
+ *
+ * @covers ::parse
+ */
+ public function testInvalidCore() {
+ $invalid_core = << [
+ 'invalid_core.info.txt' => $invalid_core,
+ ],
+ ]);
+ $filename = vfsStream::url('modules/fixtures/invalid_core.info.txt');
+ $this->expectException('\Drupal\Core\Extension\InfoParserException');
+ $this->expectExceptionMessage("Invalid 'core' value \"^8\" in vfs://modules/fixtures/invalid_core.info.txt");
+ $this->infoParser->parse($filename);
+ }
+
+ /**
+ * Tests that 'core_dependency' throws an exception if constraint is invalid.
+ *
+ * @covers ::parse
+ */
+ public function testCoreDependencyInvalid() {
+ $invalid_core_dependency = << [
+ 'invalid_core_dependency.info.txt' => $invalid_core_dependency,
+ ],
+ ]);
+ $filename = vfsStream::url('modules/fixtures/invalid_core_dependency.info.txt');
+ $this->expectException('\Drupal\Core\Extension\InfoParserException');
+ $this->expectExceptionMessage("The 'core_dependency' can not be used to specify compatibility specific version before 8.7.7 in vfs://modules/fixtures/invalid_core_dependency.info.txt");
$this->infoParser->parse($filename);
}
@@ -156,6 +303,7 @@ public function testInfoParserCommonInfo() {
$info_values = $this->infoParser->parse(vfsStream::url('modules/fixtures/common_test.info.txt'));
$this->assertEquals($info_values['simple_string'], 'A simple string', 'Simple string value was parsed correctly.');
$this->assertEquals($info_values['version'], \Drupal::VERSION, 'Constant value was parsed correctly.');
+ $this->assertSame($info_values['core_dependency'], '8.x');
$this->assertEquals($info_values['double_colon'], 'dummyClassName::method', 'Value containing double-colon was parsed correctly.');
}
diff --git a/core/tests/Drupal/Tests/WebAssert.php b/core/tests/Drupal/Tests/WebAssert.php
index 9c0ffb99d2..08c54a1a8c 100644
--- a/core/tests/Drupal/Tests/WebAssert.php
+++ b/core/tests/Drupal/Tests/WebAssert.php
@@ -491,6 +491,35 @@ public function fieldDisabled($field, TraversableElement $container = NULL) {
return $node;
}
+ /**
+ * Checks that a given form field element is enabled.
+ *
+ * @param string $field
+ * One of id|name|label|value for the field.
+ * @param \Behat\Mink\Element\TraversableElement $container
+ * (optional) The document to check against. Defaults to the current page.
+ *
+ * @return \Behat\Mink\Element\NodeElement
+ * The matching element.
+ *
+ * @throws \Behat\Mink\Exception\ElementNotFoundException
+ * @throws \Behat\Mink\Exception\ExpectationException
+ */
+ public function fieldEnabled($field, TraversableElement $container = NULL) {
+ $container = $container ?: $this->session->getPage();
+ $node = $container->findField($field);
+
+ if ($node === NULL) {
+ throw new ElementNotFoundException($this->session->getDriver(), 'field', 'id|name|label|value', $field);
+ }
+
+ if ($node->hasAttribute('disabled')) {
+ throw new ExpectationException("Field $field is not enabled", $this->session->getDriver());
+ }
+
+ return $node;
+ }
+
/**
* Checks that specific hidden field exists.
*