diff --git a/core/modules/system/src/Tests/Update/UpdatePathTestBase.php b/core/modules/system/src/Tests/Update/UpdatePathTestBase.php new file mode 100644 index 0000000..500fd41 --- /dev/null +++ b/core/modules/system/src/Tests/Update/UpdatePathTestBase.php @@ -0,0 +1,359 @@ +zlibInstalled = function_exists('gzopen'); + } + + /** + * Overrides WebTestBase::setUp() for upgrade testing. + * + * @see \Drupal\simpletest\WebTestBase::prepareDatabasePrefix() + * @see \Drupal\simpletest\WebTestBase::changeDatabasePrefix() + * @see \Drupal\simpletest\WebTestBase::prepareEnvironment() + */ + protected function setUp() { + // We are going to set a missing zlib requirement property for usage + // during the performUpgrade() and tearDown() methods. Also set that the + // tests failed. + if (!$this->zlibInstalled) { + parent::setUp(); + return; + } + + // Set the update url. + $this->updateUrl = Url::fromRoute('system.db_update'); + + // When running tests through the Simpletest UI (vs. on the command line), + // Simpletest's batch conflicts with the installer's batch. Batch API does + // not support the concept of nested batches (in which the nested is not + // progressive), so we need to temporarily pretend there was no batch. + // Backup the currently running Simpletest batch. + $this->originalBatch = batch_get(); + + // Define information about the user 1 account. + $this->rootUser = new UserSession(array( + 'uid' => 1, + 'name' => 'admin', + 'mail' => 'admin@example.com', + 'pass_raw' => $this->randomMachineName(), + )); + + // The child site derives its session name from the database prefix when + // running web tests. + $this->generateSessionName($this->databasePrefix); + + // Reset the static batch to remove Simpletest's batch operations. + $batch = &batch_get(); + $batch = array(); + + // Get parameters for install_drupal() before removing global variables. + $parameters = $this->installParameters(); + + // Prepare installer settings that are not install_drupal() parameters. + // Copy and prepare an actual settings.php, so as to resemble a regular + // installation. + // Not using File API; a potential error must trigger a PHP warning. + $directory = DRUPAL_ROOT . '/' . $this->siteDirectory; + copy(DRUPAL_ROOT . '/sites/default/default.settings.php', $directory . '/settings.php'); + copy(DRUPAL_ROOT . '/sites/default/default.services.yml', $directory . '/services.yml'); + + // All file system paths are created by System module during installation. + // @see system_requirements() + // @see TestBase::prepareEnvironment() + $settings['settings']['file_public_path'] = (object) array( + 'value' => $this->publicFilesDirectory, + 'required' => TRUE, + ); + $settings['settings']['file_private_path'] = (object) array( + 'value' => $this->privateFilesDirectory, + 'required' => TRUE, + ); + // Save the original site directory path, so that extensions in the + // site-specific directory can still be discovered in the test site + // environment. + // @see \Drupal\Core\Extension\ExtensionDiscovery::scan() + $settings['settings']['test_parent_site'] = (object) array( + 'value' => $this->originalSite, + 'required' => TRUE, + ); + // Add the parent profile's search path to the child site's search paths. + // @see \Drupal\Core\Extension\ExtensionDiscovery::getProfileDirectories() + $settings['conf']['simpletest.settings']['parent_profile'] = (object) array( + 'value' => $this->originalProfile, + 'required' => TRUE, + ); + $settings['settings']['apcu_ensure_unique_prefix'] = (object) array( + 'value' => FALSE, + 'required' => TRUE, + ); + // Remember the profile which was used. + $settings['settings']['install_profile'] = (object) array( + 'value' => $this->installProfile, + 'required' => TRUE, + ); + + // Generate a hash salt. + // @todo should this be part of the exported database somehow? + $settings['settings']['hash_salt'] = (object) array( + 'value' => Crypt::randomBytesBase64(55), + 'required' => TRUE, + ); + + // Since the installer isn't run, add the database settings here too. + $settings['databases']['default'] = (object) array( + 'value' => Database::getConnectionInfo(), + 'required' => TRUE, + ); + + $this->writeSettings($settings); + // Allow for test-specific overrides. + $settings_testing_file = DRUPAL_ROOT . '/' . $this->originalSite . '/settings.testing.php'; + if (file_exists($settings_testing_file)) { + // Copy the testing-specific settings.php overrides in place. + copy($settings_testing_file, $directory . '/settings.testing.php'); + // Add the name of the testing class to settings.php and include the + // testing specific overrides + file_put_contents($directory . '/settings.php', "\n\$test_class = '" . get_class($this) ."';\n" . 'include DRUPAL_ROOT . \'/\' . $site_path . \'/settings.testing.php\';' ."\n", FILE_APPEND); + } + $settings_services_file = DRUPAL_ROOT . '/' . $this->originalSite . '/testing.services.yml'; + if (file_exists($settings_services_file)) { + // Copy the testing-specific service overrides in place. + copy($settings_services_file, $directory . '/services.yml'); + } + if ($this->strictConfigSchema) { + // Add a listener to validate configuration schema on save. + $yaml = new Yaml(); + $content = file_get_contents($directory . '/services.yml'); + $services = $yaml->parse($content); + $services['services']['simpletest.config_schema_checker'] = [ + 'class' => 'Drupal\Core\Config\Testing\ConfigSchemaChecker', + 'arguments' => ['@config.typed'], + 'tags' => [['name' => 'event_subscriber']] + ]; + file_put_contents($directory . '/services.yml', $yaml->dump($services)); + } + // Since Drupal is bootstrapped already, install_begin_request() will not + // bootstrap again. Hence, we have to reload the newly written custom + // settings.php manually. + $class_loader = require DRUPAL_ROOT . '/autoload.php'; + Settings::initialize(DRUPAL_ROOT, $this->siteDirectory, $class_loader); + + // Load the database(s). + foreach ($this->databaseDumpFiles as $file) { + if (substr($file, -3) == '.gz') { + $file = "compress.zlib://$file"; + } + require $file; + } + + // Import new settings.php. + Settings::initialize(DRUPAL_ROOT, $this->siteDirectory, $class_loader); + + // After writing settings.php, the installer removes write permissions + // from the site directory. To allow drupal_generate_test_ua() to write + // a file containing the private key for drupal_valid_test_ua(), the site + // directory has to be writable. + // TestBase::restoreEnvironment() will delete the entire site directory. + // Not using File API; a potential error must trigger a PHP warning. + chmod($directory, 0777); + + $request = Request::createFromGlobals(); + $this->kernel = DrupalKernel::createFromRequest($request, $class_loader, 'prod', TRUE); + $this->kernel->prepareLegacyRequest($request); + // Force the container to be built from scratch instead of loaded from the + // disk. This forces us to not accidentally load the parent site. + $container = $this->kernel->rebuildContainer(); + + // Add the config directories to settings.php. + drupal_install_config_directories(); + + $config = $container->get('config.factory'); + + // Manually create and configure private and temporary files directories. + // While these could be preset/enforced in settings.php like the public + // files directory above, some tests expect them to be configurable in the + // UI. If declared in settings.php, they would no longer be configurable. + file_prepare_directory($this->privateFilesDirectory, FILE_CREATE_DIRECTORY); + file_prepare_directory($this->tempFilesDirectory, FILE_CREATE_DIRECTORY); + $config->getEditable('system.file') + ->set('path.temporary', $this->tempFilesDirectory) + ->save(); + + // Manually configure the test mail collector implementation to prevent + // tests from sending out emails and collect them in state instead. + // While this should be enforced via settings.php prior to installation, + // some tests expect to be able to test mail system implementations. + $config->getEditable('system.mail') + ->set('interface.default', 'test_mail_collector') + ->save(); + + // By default, verbosely display all errors and disable all production + // environment optimizations for all tests to avoid needless overhead and + // ensure a sane default experience for test authors. + // @see https://www.drupal.org/node/2259167 + $config->getEditable('system.logging') + ->set('error_level', 'verbose') + ->save(); + $config->getEditable('system.performance') + ->set('css.preprocess', FALSE) + ->set('js.preprocess', FALSE) + ->save(); + + // Collect modules to install. + $class = get_class($this); + $modules = array(); + while ($class) { + if (property_exists($class, 'modules')) { + $modules = array_merge($modules, $class::$modules); + } + $class = get_parent_class($class); + } + if ($modules) { + $modules = array_unique($modules); + try { + $success = $container->get('module_installer')->install($modules, TRUE); + $this->assertTrue($success, SafeMarkup::format('Enabled modules: %modules', array('%modules' => implode(', ', $modules)))); + } + catch (MissingDependencyException $e) { + // The exception message has all the details. + $this->fail($e->getMessage()); + } + + $this->rebuildContainer(); + } + + // Restore the original Simpletest batch. + $batch = &batch_get(); + $batch = $this->originalBatch; + + // Reset/rebuild all data structures after enabling the modules, primarily + // to synchronize all data structures and caches between the test runner and + // the child site. + // @see \Drupal\Core\DrupalKernel::bootCode() + // @todo Test-specific setUp() methods may set up further fixtures; find a + // way to execute this after setUp() is done, or to eliminate it entirely. + $this->resetAll(); + $this->kernel->prepareLegacyRequest($request); + + // Explicitly call register() again on the container registered in \Drupal. + // @todo This should already be called through + // DrupalKernel::prepareLegacyRequest() -> DrupalKernel::boot() but that + // appears to be calling a different container. + $this->container->get('stream_wrapper_manager')->register(); + + // Replace User 1 with the user created here. + /** @var \Drupal\user\UserInterface $account */ + $account = User::load(1); + $account->setPassword($this->rootUser->pass_raw); + $account->setEmail($this->rootUser->getEmail()); + $account->setUsername($this->rootUser->getUsername()); + $account->save(); + } + + /** + * Helper function to run pending database updates. + */ + protected function runUpdates() { + $this->drupalLogin($this->rootUser); + $this->drupalGet($this->updateUrl, ['external' => TRUE]); + $this->clickLink(t('Continue')); + + // Run the update hooks. + $this->clickLink(t('Apply pending updates')); + } + +} diff --git a/core/modules/system/src/Tests/Update/UpdatePathTestBaseTest.php b/core/modules/system/src/Tests/Update/UpdatePathTestBaseTest.php new file mode 100644 index 0000000..92a3fb4 --- /dev/null +++ b/core/modules/system/src/Tests/Update/UpdatePathTestBaseTest.php @@ -0,0 +1,56 @@ +databaseDumpFiles = [__DIR__ . '/../../../tests/fixtures/update/drupal-8.bare.standard.php.gz']; + parent::setUp(); + } + + /** + * Tests that the database was properly loaded. + */ + protected function testDatabaseLoaded() { + foreach (['user', 'node', 'system', 'update_test_schema'] as $module) { + $this->assertEqual(drupal_get_installed_schema_version($module), 8000, SafeMarkup::format('Module @module schema is 8000', ['@module' => $module])); + } + $this->assertEqual(\Drupal::config('system.site')->get('name'), 'Site-Install'); + $this->drupalGet(''); + $this->assertText('Site-Install'); + } + + /** + * Test that updates are properly run. + */ + protected function testUpdateHookN() { + // Increment the schema version. + \Drupal::state()->set('update_test_schema_version', 8001); + $this->runUpdates(); + // Ensure schema has changed. + $this->assertEqual(drupal_get_installed_schema_version('update_test_schema', TRUE), 8001); + // Ensure the index was added for column a. + $this->assertTrue(db_index_exists('update_test_schema_table', 'test'), 'Version 8001 of the update_test_schema module is installed.'); + } + +} diff --git a/core/modules/system/src/Tests/Update/UpdateSchemaTest.php b/core/modules/system/src/Tests/Update/UpdateSchemaTest.php new file mode 100644 index 0000000..b7ea05e --- /dev/null +++ b/core/modules/system/src/Tests/Update/UpdateSchemaTest.php @@ -0,0 +1,71 @@ +user = $this->drupalCreateUser(['administer software updates', 'access site in maintenance mode']); + $this->updateUrl = Url::fromRoute('system.db_update'); + } + + /** + * Tests that update hooks are properly run. + */ + function testUpdateHooks() { + // Verify that the 8000 schema is in place. + $this->assertEqual(drupal_get_installed_schema_version('update_test_schema'), 8000); + $this->assertFalse(db_index_exists('update_test_schema_table', 'test'), 'Version 8000 of the update_test_schema module is installed.'); + + // Increment the schema version. + \Drupal::state()->set('update_test_schema_version', 8001); + + $this->drupalLogin($this->user); + $this->drupalGet($this->updateUrl, ['external' => TRUE]); + $this->clickLink(t('Continue')); + $this->assertRaw('Schema version 8001.'); + // Run the update hooks. + $this->clickLink(t('Apply pending updates')); + + // Ensure schema has changed. + $this->assertEqual(drupal_get_installed_schema_version('update_test_schema', TRUE), 8001); + // Ensure the index was added for column a. + $this->assertTrue(db_index_exists('update_test_schema_table', 'test'), 'Version 8001 of the update_test_schema module is installed.'); + } + +} diff --git a/core/modules/system/tests/modules/update_test_schema/update_test_schema.info.yml b/core/modules/system/tests/modules/update_test_schema/update_test_schema.info.yml new file mode 100644 index 0000000..620fb92 --- /dev/null +++ b/core/modules/system/tests/modules/update_test_schema/update_test_schema.info.yml @@ -0,0 +1,6 @@ +name: 'Update test schema' +type: module +description: 'Support module for update testing.' +package: Testing +version: VERSION +core: 8.x diff --git a/core/modules/system/tests/modules/update_test_schema/update_test_schema.install b/core/modules/system/tests/modules/update_test_schema/update_test_schema.install new file mode 100644 index 0000000..56ce567 --- /dev/null +++ b/core/modules/system/tests/modules/update_test_schema/update_test_schema.install @@ -0,0 +1,41 @@ +get('update_test_schema_version', 8000); + $table = [ + 'fields' => [ + 'a' => ['type' => 'int', 'not null' => TRUE], + 'b' => ['type' => 'blob', 'not null' => FALSE], + ], + ]; + switch ($schema_version) { + case 8001: + // Add the index. + $table['indexes']['test'] = ['a']; + break; + } + return ['update_test_schema_table' => $table]; +} + +// Update hooks are defined depending on state as well. +$next = \Drupal::state()->get('update_test_schema_version', 8000) + 1; + +if ($next > 8001) { + /** + * Schema version 8001. + */ + function update_test_schema_update_8001() { + // Add a column. + db_add_index('update_test_schema_table', 'test', ['a']); + } +}