diff --git a/core/config/install/core.extension.yml b/core/config/install/core.extension.yml index 659446cc64..8f8fe08f60 100644 --- a/core/config/install/core.extension.yml +++ b/core/config/install/core.extension.yml @@ -1,3 +1,4 @@ module: {} theme: {} profile: '' +versions: {} diff --git a/core/config/schema/core.extension.schema.yml b/core/config/schema/core.extension.schema.yml index 087e2e3b94..c5f9a9177e 100644 --- a/core/config/schema/core.extension.schema.yml +++ b/core/config/schema/core.extension.schema.yml @@ -17,3 +17,16 @@ core.extension: profile: type: string label: 'Install profile' + versions: + type: sequence + label: 'Version information' + sequence: + type: mapping + label: 'Extensions' + mapping: + current: + type: string + label: 'Currently installed version' + schema: + type: string + label: 'Currently installed schema version' diff --git a/core/includes/schema.inc b/core/includes/schema.inc index 443cbf1934..7201fa3c2d 100644 --- a/core/includes/schema.inc +++ b/core/includes/schema.inc @@ -104,6 +104,12 @@ function drupal_get_installed_schema_version($module, $reset = FALSE, $array = F */ function drupal_set_installed_schema_version($module, $version) { \Drupal::keyValue('system.schema')->set($module, $version); + // Store the information in config so that we can ensure that database updates + // are the same between the source and the target site instances. + \Drupal::configFactory() + ->getEditable('core.extension') + ->set("versions.$module.schema", (string) $version) + ->save(); // Reset the static cache of module schema versions. drupal_get_installed_schema_version(NULL, TRUE); } diff --git a/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php index ea7d29fedf..dd774d6440 100644 --- a/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php @@ -66,14 +66,33 @@ public function onConfigImporterValidate(ConfigImporterEvent $event) { } } $config_importer = $event->getConfigImporter(); - if ($config_importer->getStorageComparer()->getSourceStorage()->exists('core.extension')) { + if ($this->validateCoreExtension($config_importer)) { $this->validateModules($config_importer); $this->validateThemes($config_importer); $this->validateDependencies($config_importer); } - else { + } + + /** + * Validates the core.extension configuration file. + * + * @param \Drupal\Core\Config\ConfigImporter $config_importer + * The configuration importer. + * + * @return bool + * TRUE if the core.extension is valid, FALSE if not. + */ + protected function validateCoreExtension(ConfigImporter $config_importer) { + $data = $config_importer->getStorageComparer()->getSourceStorage()->read('core.extension'); + if ($data === FALSE) { $config_importer->logError($this->t('The core.extension configuration does not exist.')); + return FALSE; } + elseif (empty($data['versions'])) { + $config_importer->logError($this->t('The core.extension file is out-of-date. Update the codebase of the source site, run update.php and export the configuration again.')); + return FALSE; + } + return TRUE; } /** @@ -91,6 +110,25 @@ protected function validateModules(ConfigImporter $config_importer) { $config_importer->logError($this->t('Unable to install the %module module since it does not exist.', ['%module' => $module])); } + // Ensure that schema and version of the existing modules that produced the + // source configuration match the site. + $existing_modules = array_keys(array_intersect_key($core_extension['module'], $config_importer->getStorageComparer()->getTargetStorage()->read('core.extension')['module'])); + foreach ($existing_modules as $module) { + if ($core_extension['versions'][$module]['current'] !== $module_data[$module]->info['version']) { + $config_importer->logError($this->t( + 'The version of %module module on the source site (@source_version) does not match this site (@target_version).', + ['%module' => $module, '@source_version' => $core_extension['versions'][$module]['current'], '@target_version' => $module_data[$module]->info['version']] + )); + } + elseif ($core_extension['versions'][$module]['schema'] !== (string) drupal_get_installed_schema_version($module)) { + $config_importer->logError($this->t( + 'The schema version of %module module on the source site (@source_schema) does not match this site (@target_schema).', + ['%module' => $module, '@source_schema' => $core_extension['versions'][$module]['schema'], '@target_schema' => drupal_get_installed_schema_version($module)] + )); + + } + } + // Ensure that all modules being installed have their dependencies met. $installs = $config_importer->getExtensionChangelist('module', 'install'); foreach ($installs as $module) { @@ -109,6 +147,12 @@ protected function validateModules(ConfigImporter $config_importer) { ); $config_importer->logError($message); } + if ($core_extension['versions'][$module]['current'] !== $module_data[$module]->info['version']) { + $config_importer->logError($this->t( + 'Unable to install the %module module since the installed version (@config_version) does not match the code base (@code_version).', + ['%module' => $module, '@config_version' => $core_extension['versions'][$module]['current'], '@code_version' => $module_data[$module]->info['version']] + )); + } } // Get the install profile from the site's configuration. @@ -160,6 +204,18 @@ protected function validateThemes(ConfigImporter $config_importer) { } } + // Ensure that version of the existing themes that produced the source + // configuration match the site. + $existing_themes = array_keys(array_intersect_key($core_extension['theme'], $config_importer->getStorageComparer()->getTargetStorage()->read('core.extension')['theme'])); + foreach ($existing_themes as $theme) { + if ($core_extension['versions'][$theme]['current'] !== $theme_data[$theme]->info['version']) { + $config_importer->logError($this->t( + 'The version of %theme theme on the source site (@source_version) does not match this site (@target_version).', + ['%theme' => $theme, '@source_version' => $core_extension['versions'][$theme]['current'], '@target_version' => $theme_data[$theme]->info['version']] + )); + } + } + // Ensure that all themes being installed have their dependencies met. foreach ($installs as $theme) { foreach (array_keys($theme_data[$theme]->requires) as $required_theme) { @@ -169,6 +225,12 @@ protected function validateThemes(ConfigImporter $config_importer) { $config_importer->logError($this->t('Unable to install the %theme theme since it requires the %required_theme theme.', ['%theme' => $theme_name, '%required_theme' => $required_theme_name])); } } + if ($core_extension['versions'][$theme]['current'] !== $theme_data[$theme]->info['version']) { + $config_importer->logError($this->t( + 'Unable to install the %theme theme since the installed version (@config_version) does not match the code base (@code_version).', + ['%theme' => $theme, '@config_version' => $core_extension['versions'][$theme]['current'], '@code_version' => $theme_data[$theme]->info['version']] + )); + } } // Ensure that all themes being uninstalled are not required by themes that diff --git a/core/lib/Drupal/Core/Extension/InfoParserDynamic.php b/core/lib/Drupal/Core/Extension/InfoParserDynamic.php index 3b00cea093..e8e7a49a38 100644 --- a/core/lib/Drupal/Core/Extension/InfoParserDynamic.php +++ b/core/lib/Drupal/Core/Extension/InfoParserDynamic.php @@ -31,6 +31,9 @@ public function parse($filename) { if (isset($parsed_info['version']) && $parsed_info['version'] === 'VERSION') { $parsed_info['version'] = \Drupal::VERSION; } + if (!isset($parsed_info['version'])) { + $parsed_info['version'] = 'None'; + } } return $parsed_info; } diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php index 2cf34bd22f..67e445d53f 100644 --- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php +++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php @@ -309,6 +309,20 @@ public function install(array $module_list, $enable_dependencies = TRUE) { // If any modules were newly installed, invoke hook_modules_installed(). if (!empty($modules_installed)) { + // There will be a new config factory. + $extension_config = \Drupal::configFactory()->getEditable('core.extension'); + // Update version information in configuration. + $list = $this->moduleHandler->getModuleList(); + $versions = $extension_config->get('versions'); + foreach ($modules_installed as $module_name) { + $info = \Drupal::service('info_parser')->parse($list[$module_name]->getPathname()); + $versions[$module_name] = [ + 'current' => $info['version'], + 'schema' => (string) drupal_get_installed_schema_version($module_name), + ]; + } + ksort($versions); + $extension_config->set('versions', $versions)->save(); \Drupal::getContainer()->set('router.route_provider', \Drupal::service('router.route_provider.old')); if (!\Drupal::service('router.route_provider.lazy_builder')->hasRebuilt()) { // Rebuild routes after installing module. This is done here on top of @@ -436,7 +450,11 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) { // Remove the module's entry from the config. Don't check schema when // uninstalling a module since we are only clearing a key. - \Drupal::configFactory()->getEditable('core.extension')->clear("module.$module")->save(TRUE); + \Drupal::configFactory() + ->getEditable('core.extension') + ->clear("module.$module") + ->clear("versions.$module") + ->save(TRUE); // Update the module handler to remove the module. // The current ModuleHandler instance is obsolete with the kernel rebuild diff --git a/core/lib/Drupal/Core/Extension/ThemeInstaller.php b/core/lib/Drupal/Core/Extension/ThemeInstaller.php index 43a5469e3f..a496372c24 100644 --- a/core/lib/Drupal/Core/Extension/ThemeInstaller.php +++ b/core/lib/Drupal/Core/Extension/ThemeInstaller.php @@ -169,8 +169,14 @@ public function install(array $theme_list, $install_dependencies = TRUE) { // The value is not used; the weight is ignored for themes currently. Do // not check schema when saving the configuration. + $versions = $extension_config->get('versions'); + $versions[$key] = [ + 'current' => $theme_data[$key]->info['version'], + ]; + ksort($versions); $extension_config ->set("theme.$key", 0) + ->set('versions', $versions) ->save(TRUE); // Add the theme to the current list. @@ -247,7 +253,9 @@ public function uninstall(array $theme_list) { $current_theme_data = $this->state->get('system.theme.data', []); foreach ($theme_list as $key) { // The value is not used; the weight is ignored for themes currently. - $extension_config->clear("theme.$key"); + $extension_config + ->clear("theme.$key") + ->clear("versions.$key"); // Update the current theme data accordingly. unset($current_theme_data[$key]); diff --git a/core/modules/config/tests/src/Functional/ConfigImportInstallProfileTest.php b/core/modules/config/tests/src/Functional/ConfigImportInstallProfileTest.php index 8b0787d6ee..867c47c273 100644 --- a/core/modules/config/tests/src/Functional/ConfigImportInstallProfileTest.php +++ b/core/modules/config/tests/src/Functional/ConfigImportInstallProfileTest.php @@ -62,8 +62,13 @@ public function testInstallProfileValidation() { $core['module']['testing_config_import'] = 0; unset($core['module']['syslog']); unset($core['theme']['stark']); + unset($core['versions']['syslog']); + unset($core['versions']['stark']); $core['theme']['stable'] = 0; $core['theme']['classy'] = 0; + $core['versions']['stable'] = ['current' => \Drupal::VERSION]; + $core['versions']['classy'] = ['current' => \Drupal::VERSION]; + ksort($core['versions']); $sync->write('core.extension', $core); $sync->deleteAll('syslog.'); $theme = $sync->read('system.theme'); diff --git a/core/modules/config/tests/src/Functional/ConfigImportUITest.php b/core/modules/config/tests/src/Functional/ConfigImportUITest.php index fd76990da6..acb51a3b31 100644 --- a/core/modules/config/tests/src/Functional/ConfigImportUITest.php +++ b/core/modules/config/tests/src/Functional/ConfigImportUITest.php @@ -79,9 +79,14 @@ public function testImport() { $core_extension['module']['action'] = 0; $core_extension['module']['ban'] = 0; $core_extension['module'] = module_config_sort($core_extension['module']); + $core_extension['versions']['action'] = ['current' => \Drupal::VERSION, 'schema' => '8000']; + $core_extension['versions']['ban'] = ['current' => \Drupal::VERSION, 'schema' => '8000']; // Bartik is a subtheme of classy so classy must be enabled. $core_extension['theme']['classy'] = 0; $core_extension['theme']['bartik'] = 0; + $core_extension['versions']['classy'] = ['current' => \Drupal::VERSION]; + $core_extension['versions']['bartik'] = ['current' => \Drupal::VERSION]; + ksort($core_extension['versions']); $sync->write('core.extension', $core_extension); // Use the install storage so that we can read configuration from modules @@ -171,6 +176,11 @@ public function testImport() { unset($core_extension['module']['options']); unset($core_extension['module']['text']); unset($core_extension['theme']['bartik']); + unset($core_extension['versions']['action']); + unset($core_extension['versions']['ban']); + unset($core_extension['versions']['options']); + unset($core_extension['versions']['text']); + unset($core_extension['versions']['bartik']); $sync->write('core.extension', $core_extension); $sync->delete('action.settings'); $sync->delete('text.settings'); @@ -508,6 +518,16 @@ public function testExtensionValidation() { $core['module']['does_not_exist'] = 0; // This theme does not exist. $core['theme']['does_not_exist'] = 0; + // This module does exist but the version is wrong. + $core['module']['ban'] = 0; + $core['versions']['ban'] = ['current' => '8.0.0-rc3', 'schema' => '8000']; + // This theme does exist but the version is wrong. + $core['theme']['seven'] = 0; + $core['versions']['seven'] = ['current' => '8.0.0-rc3']; + $core['versions']['bartik']['current'] = '8.0.0-rc3'; + $core['versions']['text']['current'] = '8.0.0-rc3'; + $core['versions']['config_test']['schema'] = '8999'; + $sync->write('core.extension', $core); $this->drupalPostForm('admin/config/development/configuration', [], t('Import all')); @@ -516,6 +536,10 @@ public function testExtensionValidation() { $this->assertText('Unable to uninstall the Classy theme since the Bartik theme is installed.'); $this->assertText('Unable to install the does_not_exist module since it does not exist.'); $this->assertText('Unable to install the does_not_exist theme since it does not exist.'); + $this->assertText('Unable to install the ban module since the installed version (8.0.0-rc3) does not match the code base (' . \Drupal::VERSION . ').'); + $this->assertText('Unable to install the seven theme since the installed version (8.0.0-rc3) does not match the code base (' . \Drupal::VERSION . ').'); + $this->assertText('The schema version of config_test module on the source site (8999) does not match this site (' . drupal_get_installed_schema_version('config_test') . ').'); + $this->assertText('The version of bartik theme on the source site (8.0.0-rc3) does not match this site (' . \Drupal::VERSION . ').'); } } diff --git a/core/modules/simpletest/src/KernelTestBase.php b/core/modules/simpletest/src/KernelTestBase.php index 255adcc256..e034403398 100644 --- a/core/modules/simpletest/src/KernelTestBase.php +++ b/core/modules/simpletest/src/KernelTestBase.php @@ -256,7 +256,12 @@ protected function setUp() { // \Drupal\Core\Config\ConfigInstaller::installDefaultConfig() to work. // Write directly to active storage to avoid early instantiation of // the event dispatcher which can prevent modules from registering events. - \Drupal::service('config.storage')->write('core.extension', ['module' => [], 'theme' => [], 'profile' => '']); + \Drupal::service('config.storage')->write('core.extension', [ + 'module' => [], + 'theme' => [], + 'versions' => [], + 'profile' => '', + ]); // Collect and set a fixed module list. $class = get_class($this); @@ -550,6 +555,11 @@ protected function enableModules(array $modules) { $module_handler->addModule($module, $module_list[$module]->getPath()); // Maintain the list of enabled modules in configuration. $extensions['module'][$module] = 0; + $version = \Drupal::service('info_parser')->parse($module_list[$module]->getPathname())['version']; + $extensions['versions'][$module] = [ + 'current' => $version, + 'schema' => (string) SCHEMA_UNINSTALLED, + ]; } $active_storage->write('core.extension', $extensions); diff --git a/core/modules/simpletest/src/Tests/KernelTestBaseTest.php b/core/modules/simpletest/src/Tests/KernelTestBaseTest.php index 1e9e75da0b..fe1cb1e8df 100644 --- a/core/modules/simpletest/src/Tests/KernelTestBaseTest.php +++ b/core/modules/simpletest/src/Tests/KernelTestBaseTest.php @@ -332,6 +332,18 @@ public function testDrupalGetProfile() { $this->assertNull(\Drupal::installProfile()); } + /** + * @covers ::enableModules + */ + public function testEnableModules() { + $this->assertFalse(\Drupal::moduleHandler()->moduleExists('system')); + $this->enableModules(['system', 'user']); + $core = \Drupal::config('core.extension'); + $this->assertEqual(['entity_test' => 0, 'user' => 0, 'system' => 0], $core->get('module')); + $this->assertEqual(['current' => \Drupal::VERSION, 'schema' => (string) SCHEMA_UNINSTALLED], $core->get('versions.user')); + $this->assertTrue(\Drupal::moduleHandler()->moduleExists('system')); + } + /** * {@inheritdoc} */ diff --git a/core/modules/system/src/Controller/DbUpdateController.php b/core/modules/system/src/Controller/DbUpdateController.php index 9e0a374a4a..08f5c21cb6 100644 --- a/core/modules/system/src/Controller/DbUpdateController.php +++ b/core/modules/system/src/Controller/DbUpdateController.php @@ -649,6 +649,17 @@ public static function batchFinished($success, $results, $operations) { // No updates to run, so caches won't get flushed later. Clear them now. drupal_flush_all_caches(); + // Update core.extensions to have the correct version information. + $core = \Drupal::configFactory()->getEditable('core.extension'); + foreach (\Drupal::moduleHandler()->getModuleList() as $module => $extension) { + $version = \Drupal::service('info_parser')->parse($extension->getPathname())['version']; + $core->set("versions.$module.current", $version); + } + foreach (\Drupal::service('theme_handler')->listInfo() as $theme => $extension) { + $version = \Drupal::service('info_parser')->parse($extension->getPathname())['version']; + $core->set("versions.$theme.current", $version); + } + $_SESSION['update_results'] = $results; $_SESSION['update_success'] = $success; $_SESSION['updates_remaining'] = $operations; diff --git a/core/modules/system/src/Tests/Update/VersionInfoUpdateTest.php b/core/modules/system/src/Tests/Update/VersionInfoUpdateTest.php new file mode 100644 index 0000000000..737dba463f --- /dev/null +++ b/core/modules/system/src/Tests/Update/VersionInfoUpdateTest.php @@ -0,0 +1,55 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz', + ]; + } + + /** + * Tests that core.extension:versions is updated properly. + */ + public function testSystemUpdate8015() { + // Set a partial value to ensure that regardless everything is updated + // correctly. + $this->config('core.extension')->set('versions.user.current', '8.0.0')->save(); + $this->runUpdates(); + + // Test that a module is added. + $this->assertEqual([ + 'current' => \Drupal::VERSION, + 'schema' => drupal_get_installed_schema_version('system'), + ], $this->config('core.extension')->get('versions.system')); + // Test that a profile is added. + $this->assertEqual([ + 'current' => \Drupal::VERSION, + 'schema' => drupal_get_installed_schema_version('standard'), + ], $this->config('core.extension')->get('versions.standard')); + // Test that a theme is added. + $this->assertEqual([ + 'current' => \Drupal::VERSION, + ], $this->config('core.extension')->get('versions.bartik')); + + // Test that any existing version information is merged correctly. + $this->assertEqual([ + 'current' => \Drupal::VERSION, + 'schema' => drupal_get_installed_schema_version('user'), + ], $this->config('core.extension')->get('versions.user')); + + } + +} diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 1838326451..6c03f93fcf 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -18,6 +18,7 @@ use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\DrupalKernel; +use Drupal\Core\Extension\Extension; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Site\Settings; use Drupal\Core\StreamWrapper\PrivateStream; @@ -2135,3 +2136,37 @@ function system_update_8501() { } } } + +/** + * Add the version information to core.extension + */ +function system_update_8600() { + // Update core.extensions to have the correct version information. + $core = \Drupal::configFactory()->getEditable('core.extension'); + $versions = $core->get('versions') ?: []; + $info_parser = \Drupal::service('info_parser'); + + // Create a method to populate the version information for an array of + // \Drupal\Core\Extension\Extension objects. + $map_version_info = function (Extension $extension) use ($info_parser) { + // Read from disk to avoid using APIs. + $version = $info_parser->parse($extension->getPathname())['version']; + $return = [ + 'current' => $version, + ]; + // Modules and profiles also stored the schema version. + if ($extension->getType() === 'module' || $extension->getType() === 'profile') { + $return['schema'] = (string) drupal_get_installed_schema_version($extension->getName()); + } + return $return; + }; + + // It is possible that version information already exists. For example, + // another hook_update_N hook could have installed a module. However we can + // safely ignore that as this will ensure that all the information is correct + // at the time of writing. + $versions = array_map($map_version_info, \Drupal::moduleHandler()->getModuleList()) + + array_map($map_version_info, \Drupal::service('theme_handler')->listInfo()); + ksort($versions); + $core->set('versions', $versions)->save(); +} diff --git a/core/modules/system/tests/src/Functional/Module/UninstallTest.php b/core/modules/system/tests/src/Functional/Module/UninstallTest.php index 2b17b8fcf6..509ffa8070 100644 --- a/core/modules/system/tests/src/Functional/Module/UninstallTest.php +++ b/core/modules/system/tests/src/Functional/Module/UninstallTest.php @@ -66,6 +66,10 @@ public function testUninstallPage() { // Delete the node to allow node to be uninstalled. $node->delete(); + // Ensure the module information is present in core.extension before + // uninstall. + $this->assertNotNull($this->config('core.extension')->get('versions.module_test'), 'The "module_test" version information is present.'); + // Uninstall module_test. $edit = []; $edit['uninstall[module_test]'] = TRUE; @@ -76,6 +80,10 @@ public function testUninstallPage() { $this->drupalPostForm(NULL, NULL, t('Uninstall')); $this->assertText(t('The selected modules have been uninstalled.'), 'Modules status has been updated.'); + // Ensure the module information has been removed from core.extension after + // uninstall. + $this->assertNull($this->config('core.extension')->get('versions.module_test'), 'The "module_test" version information has been removed.'); + // Uninstall node testing that the configuration that will be deleted is // listed. $node_dependencies = \Drupal::service('config.manager')->findConfigEntityDependentsAsEntities('module', ['node']); diff --git a/core/modules/system/tests/src/Functional/Update/UpdateScriptTest.php b/core/modules/system/tests/src/Functional/Update/UpdateScriptTest.php index 29d4d08e23..4470cf5f01 100644 --- a/core/modules/system/tests/src/Functional/Update/UpdateScriptTest.php +++ b/core/modules/system/tests/src/Functional/Update/UpdateScriptTest.php @@ -197,11 +197,13 @@ public function testSuccessfulUpdateFunctionality() { // Reset the static cache to ensure we have the most current setting. $schema_version = drupal_get_installed_schema_version('update_script_test', TRUE); $this->assertEqual($schema_version, 8001, 'update_script_test schema version is 8001 after updating.'); + $this->assertIdentical('8001', $this->config('core.extension')->get('versions.update_script_test.schema'), 'update_script_test schema version is 8001 in core.extension config after updating.'); // Set the installed schema version to one less than the current update. drupal_set_installed_schema_version('update_script_test', $schema_version - 1); $schema_version = drupal_get_installed_schema_version('update_script_test', TRUE); $this->assertEqual($schema_version, 8000, 'update_script_test schema version overridden to 8000.'); + $this->assertIdentical('8000', $this->config('core.extension')->get('versions.update_script_test.schema'), 'update_script_test schema version is overridden to 8000 in core.extension config.'); // Click through update.php with 'access administration pages' and // 'access site reports' permissions. diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php index 4024b7f9d9..b14d3c24bb 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php @@ -573,6 +573,8 @@ public function testUnmetDependency() { // Add a module and a theme that depend on uninstalled extensions. $extensions['module']['book'] = 0; $extensions['theme']['bartik'] = 0; + $extensions['versions']['book'] = ['current' => \Drupal::VERSION]; + $extensions['versions']['bartik'] = ['current' => \Drupal::VERSION]; $sync->write('core.extension', $extensions); try { @@ -643,6 +645,58 @@ public function testMissingCoreExtension() { } } + /** + * Tests missing core.extension:versions during configuration import. + * + * Configuration exported before the versions information was added should + * fail. + * + * @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber + */ + public function testMissingVersionInformation() { + $sync = $this->container->get('config.storage.sync'); + $data = $sync->read('core.extension'); + unset($data['versions']); + $sync->write('core.extension', $data); + try { + $this->configImporter->reset()->import(); + $this->fail('ConfigImporterException not thrown, invalid import was not stopped due to missing version information.'); + } + catch (ConfigImporterException $e) { + $this->assertEqual($e->getMessage(), 'There were errors validating the config synchronization.'); + $error_log = $this->configImporter->getErrors(); + $this->assertEqual(['The core.extension file is out-of-date. Update the codebase of the source site, run update.php and export the configuration again.'], $error_log); + } + } + + /** + * Tests missing core.extension:versions during configuration import. + * + * Configuration exported before the versions information was added should + * fail. + * + * @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber + */ + public function testVersionMismatch() { + $sync = $this->container->get('config.storage.sync'); + $data = $sync->read('core.extension'); + $data['versions']['system']['current'] = '8.0.0-beta1'; + $data['versions']['config_test']['schema'] = '8999'; + $sync->write('core.extension', $data); + try { + $this->configImporter->reset()->import(); + $this->fail('ConfigImporterException not thrown, invalid import was not stopped due to missing version information.'); + } + catch (ConfigImporterException $e) { + $this->assertEqual($e->getMessage(), 'There were errors validating the config synchronization.'); + $error_log = $this->configImporter->getErrors(); + $this->assertEqual([ + 'The schema version of config_test module on the source site (8999) does not match this site (-1).', + 'The version of system module on the source site (8.0.0-beta1) does not match this site (' . \Drupal::VERSION . ').' + ], $error_log); + } + } + /** * Tests install profile validation during configuration import. * diff --git a/core/tests/Drupal/KernelTests/Core/Theme/ThemeInstallerTest.php b/core/tests/Drupal/KernelTests/Core/Theme/ThemeInstallerTest.php index 61f647528d..9f696db58e 100644 --- a/core/tests/Drupal/KernelTests/Core/Theme/ThemeInstallerTest.php +++ b/core/tests/Drupal/KernelTests/Core/Theme/ThemeInstallerTest.php @@ -267,6 +267,7 @@ public function testUninstall() { $this->themeInstaller()->install([$name]); $this->assertTrue($this->config("$name.settings")->get()); + $this->assertNotNull($this->config('core.extension')->get('versions.test_basetheme'), 'The "test_basetheme" version information is present after install.'); $this->themeInstaller()->uninstall([$name]); @@ -274,6 +275,7 @@ public function testUninstall() { $this->assertFalse(array_keys(system_list('theme'))); $this->assertFalse($this->config("$name.settings")->get()); + $this->assertNull($this->config('core.extension')->get('versions.test_basetheme'), 'The "test_basetheme" version information is not present after uninstall.'); // Ensure that the uninstalled theme can be installed again. $this->themeInstaller()->install([$name]); diff --git a/core/tests/Drupal/KernelTests/KernelTestBase.php b/core/tests/Drupal/KernelTests/KernelTestBase.php index 0a611d87c2..95ab85837a 100644 --- a/core/tests/Drupal/KernelTests/KernelTestBase.php +++ b/core/tests/Drupal/KernelTests/KernelTestBase.php @@ -387,12 +387,28 @@ private function bootKernel() { // Setup the destion to the be frontpage by default. \Drupal::destination()->set('/'); + // Get the module version information. + $listing = new ExtensionDiscovery(\Drupal::root()); + $module_list = $listing->scan('module'); + // In ModuleHandlerTest we pass in a profile as if it were a module. + $module_list += $listing->scan('profile'); + + $versions = []; + foreach ($modules as $module) { + $version = \Drupal::service('info_parser')->parse($module_list[$module]->getPathname())['version']; + $versions[$module] = [ + 'current' => $version, + 'schema' => (string) SCHEMA_UNINSTALLED, + ]; + } + // Write the core.extension configuration. // Required for ConfigInstaller::installDefaultConfig() to work. $this->container->get('config.storage')->write('core.extension', [ 'module' => array_fill_keys($modules, 0), 'theme' => [], 'profile' => '', + 'versions' => $versions, ]); $settings = Settings::getAll(); @@ -834,6 +850,11 @@ protected function enableModules(array $modules) { $module_handler->addModule($module, $module_list[$module]->getPath()); // Maintain the list of enabled modules in configuration. $extension_config['module'][$module] = 0; + $version = \Drupal::service('info_parser')->parse($module_list[$module]->getPathname())['version']; + $extension_config['versions'][$module] = [ + 'current' => $version, + 'schema' => (string) SCHEMA_UNINSTALLED, + ]; } $active_storage->write('core.extension', $extension_config); diff --git a/core/tests/Drupal/KernelTests/KernelTestBaseTest.php b/core/tests/Drupal/KernelTests/KernelTestBaseTest.php index 2ba85e92e1..4ab049d4e6 100644 --- a/core/tests/Drupal/KernelTests/KernelTestBaseTest.php +++ b/core/tests/Drupal/KernelTests/KernelTestBaseTest.php @@ -281,6 +281,18 @@ public function testRequiresModule() { } } + /** + * @covers ::enableModules + */ + public function testEnableModules() { + $this->assertFalse(\Drupal::moduleHandler()->moduleExists('system')); + $this->enableModules(['system', 'user']); + $core = \Drupal::config('core.extension'); + $this->assertEquals(['user' => 0, 'system' => 0], $core->get('module')); + $this->assertEquals(['current' => \Drupal::VERSION, 'schema' => (string) SCHEMA_UNINSTALLED], $core->get('versions.user')); + $this->assertTrue(\Drupal::moduleHandler()->moduleExists('system')); + } + /** * {@inheritdoc} */ diff --git a/core/tests/Drupal/Tests/Core/Extension/DefaultConfigTest.php b/core/tests/Drupal/Tests/Core/Extension/DefaultConfigTest.php index 29677b2c6e..590a5ce6e7 100644 --- a/core/tests/Drupal/Tests/Core/Extension/DefaultConfigTest.php +++ b/core/tests/Drupal/Tests/Core/Extension/DefaultConfigTest.php @@ -28,6 +28,7 @@ public function testConfigIsEmpty() { 'module' => [], 'theme' => [], 'profile' => '', + 'versions' => [], ]; $this->assertEquals($expected, $config); }