diff --git a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php index e9d8d6686e..f9f97ecaa8 100644 --- a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php +++ b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php @@ -6,11 +6,13 @@ use Drupal\Component\Utility\SafeMarkup; use Drupal\Core\Cache\Cache; use Drupal\Core\Config\Development\ConfigSchemaChecker; +use Drupal\Core\Database\Database; use Drupal\Core\DrupalKernel; use Drupal\Core\Extension\MissingDependencyException; use Drupal\Core\Serialization\Yaml; use Drupal\Core\Session\UserSession; use Drupal\Core\Site\Settings; +use Drupal\Core\StreamWrapper\StreamWrapperInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Yaml\Yaml as SymfonyYaml; @@ -448,4 +450,210 @@ protected function rebuildAll() { $this->container->get('stream_wrapper_manager')->register(); } + /** + * Returns the parameters that will be used when Simpletest installs Drupal. + * + * @see install_drupal() + * @see install_state_defaults() + * + * @return array + * Array of parameters for use in install_drupal(). + */ + protected function installParameters() { + $connection_info = Database::getConnectionInfo(); + $driver = $connection_info['default']['driver']; + $connection_info['default']['prefix'] = $connection_info['default']['prefix']['default']; + unset($connection_info['default']['driver']); + unset($connection_info['default']['namespace']); + unset($connection_info['default']['pdo']); + unset($connection_info['default']['init_commands']); + // Remove database connection info that is not used by SQLite. + if ($driver === 'sqlite') { + unset($connection_info['default']['username']); + unset($connection_info['default']['password']); + unset($connection_info['default']['host']); + unset($connection_info['default']['port']); + } + $parameters = [ + 'interactive' => FALSE, + 'parameters' => [ + 'profile' => $this->profile, + 'langcode' => 'en', + ], + 'forms' => [ + 'install_settings_form' => [ + 'driver' => $driver, + $driver => $connection_info['default'], + ], + 'install_configure_form' => [ + 'site_name' => 'Drupal', + 'site_mail' => 'simpletest@example.com', + 'account' => [ + 'name' => $this->rootUser->name, + 'mail' => $this->rootUser->getEmail(), + 'pass' => [ + 'pass1' => isset($this->rootUser->pass_raw) ? $this->rootUser->pass_raw : $this->rootUser->passRaw, + 'pass2' => isset($this->rootUser->pass_raw) ? $this->rootUser->pass_raw : $this->rootUser->passRaw, + ], + ], + // form_type_checkboxes_value() requires NULL instead of FALSE values + // for programmatic form submissions to disable a checkbox. + 'enable_update_status_module' => NULL, + 'enable_update_status_emails' => NULL, + ], + ], + ]; + + // If we only have one db driver available, we cannot set the driver. + include_once DRUPAL_ROOT . '/core/includes/install.inc'; + if (count($this->getDatabaseTypes()) == 1) { + unset($parameters['forms']['install_settings_form']['driver']); + } + return $parameters; + } + + /** + * Sets up the base URL based upon the environment variable. + * + * @throws \Exception + * Thrown when no SIMPLETEST_BASE_URL environment variable is provided. + */ + protected function setupBaseUrl() { + global $base_url; + + // Get and set the domain of the environment we are running our test + // coverage against. + $base_url = getenv('SIMPLETEST_BASE_URL'); + if (!$base_url) { + throw new \Exception( + 'You must provide a SIMPLETEST_BASE_URL environment variable to run some PHPUnit based functional tests.' + ); + } + + // Setup $_SERVER variable. + $parsed_url = parse_url($base_url); + $host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''); + $path = isset($parsed_url['path']) ? rtrim(rtrim($parsed_url['path']), '/') : ''; + $port = isset($parsed_url['port']) ? $parsed_url['port'] : 80; + + $this->baseUrl = $base_url; + + // If the passed URL schema is 'https' then setup the $_SERVER variables + // properly so that testing will run under HTTPS. + if ($parsed_url['scheme'] === 'https') { + $_SERVER['HTTPS'] = 'on'; + } + $_SERVER['HTTP_HOST'] = $host; + $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; + $_SERVER['SERVER_ADDR'] = '127.0.0.1'; + $_SERVER['SERVER_PORT'] = $port; + $_SERVER['SERVER_SOFTWARE'] = NULL; + $_SERVER['SERVER_NAME'] = 'localhost'; + $_SERVER['REQUEST_URI'] = $path . '/'; + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['SCRIPT_NAME'] = $path . '/index.php'; + $_SERVER['SCRIPT_FILENAME'] = $path . '/index.php'; + $_SERVER['PHP_SELF'] = $path . '/index.php'; + $_SERVER['HTTP_USER_AGENT'] = 'Drupal command line'; + } + + /** + * Prepares the current environment for running the test. + * + * Also sets up new resources for the testing environment, such as the public + * filesystem and configuration directories. + * + * This method is private as it must only be called once by + * BrowserTestBase::setUp() (multiple invocations for the same test would have + * unpredictable consequences) and it must not be callable or overridable by + * test classes. + */ + protected function prepareEnvironment() { + // Bootstrap Drupal so we can use Drupal's built in functions. + $this->classLoader = require __DIR__ . '/../../../../../autoload.php'; + $request = Request::createFromGlobals(); + $kernel = TestRunnerKernel::createFromRequest($request, $this->classLoader); + // TestRunnerKernel expects the working directory to be DRUPAL_ROOT. + chdir(DRUPAL_ROOT); + $kernel->prepareLegacyRequest($request); + $this->prepareDatabasePrefix(); + + $this->originalSite = $kernel->findSitePath($request); + + // Create test directory ahead of installation so fatal errors and debug + // information can be logged during installation process. + file_prepare_directory($this->siteDirectory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + + // Prepare filesystem directory paths. + $this->publicFilesDirectory = $this->siteDirectory . '/files'; + $this->privateFilesDirectory = $this->siteDirectory . '/private'; + $this->tempFilesDirectory = $this->siteDirectory . '/temp'; + $this->translationFilesDirectory = $this->siteDirectory . '/translations'; + + // Ensure the configImporter is refreshed for each test. + $this->configImporter = NULL; + + // Unregister all custom stream wrappers of the parent site. + $wrappers = \Drupal::service('stream_wrapper_manager')->getWrappers(StreamWrapperInterface::ALL); + foreach ($wrappers as $scheme => $info) { + stream_wrapper_unregister($scheme); + } + + // Reset statics. + drupal_static_reset(); + + $this->container = NULL; + + // Unset globals. + unset($GLOBALS['config_directories']); + unset($GLOBALS['config']); + unset($GLOBALS['conf']); + + // Log fatal errors. + ini_set('log_errors', 1); + ini_set('error_log', DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log'); + + // Change the database prefix. + $this->changeDatabasePrefix(); + + // After preparing the environment and changing the database prefix, we are + // in a valid test environment. + drupal_valid_test_ua($this->databasePrefix); + + // Reset settings. + new Settings([ + // For performance, simply use the database prefix as hash salt. + 'hash_salt' => $this->databasePrefix, + ]); + + drupal_set_time_limit($this->timeLimit); + + // Save and clean the shutdown callbacks array because it is static cached + // and will be changed by the test run. Otherwise it will contain callbacks + // from both environments and the testing environment will try to call the + // handlers defined by the original one. + $callbacks = &drupal_register_shutdown_function(); + $this->originalShutdownCallbacks = $callbacks; + $callbacks = []; + } + + /** + * Returns all supported database driver installer objects. + * + * This wraps drupal_get_database_types() for use without a current container. + * + * @return \Drupal\Core\Database\Install\Tasks[] + * An array of available database driver installer objects. + */ + protected function getDatabaseTypes() { + if ($this->originalContainer) { + \Drupal::setContainer($this->originalContainer); + } + $database_types = drupal_get_database_types(); + if ($this->originalContainer) { + \Drupal::unsetContainer(); + } + return $database_types; + } + } diff --git a/core/lib/Drupal/Core/Test/TestSetupTrait.php b/core/lib/Drupal/Core/Test/TestSetupTrait.php index ea9137e479..44409078db 100644 --- a/core/lib/Drupal/Core/Test/TestSetupTrait.php +++ b/core/lib/Drupal/Core/Test/TestSetupTrait.php @@ -141,7 +141,7 @@ public static function getDatabaseConnection() { * @see \Drupal\simpletest\TestBase::prepareEnvironment() * @see drupal_valid_test_ua() */ - private function prepareDatabasePrefix() { + protected function prepareDatabasePrefix() { $test_db = new TestDatabase(); $this->siteDirectory = $test_db->getTestSitePath(); $this->databasePrefix = $test_db->getDatabasePrefix(); @@ -150,7 +150,7 @@ private function prepareDatabasePrefix() { /** * Changes the database connection to the prefixed one. */ - private function changeDatabasePrefix() { + protected function changeDatabasePrefix() { if (empty($this->databasePrefix)) { $this->prepareDatabasePrefix(); } @@ -158,7 +158,7 @@ private function changeDatabasePrefix() { // If the test is run with argument dburl then use it. $db_url = getenv('SIMPLETEST_DB'); if (!empty($db_url)) { - $database = Database::convertDbUrlToConnectionInfo($db_url, DRUPAL_ROOT); + $database = Database::convertDbUrlToConnectionInfo($db_url, isset($this->root) ? $this->root : DRUPAL_ROOT); Database::addConnectionInfo('default', 'default', $database); } diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php index 975d95bd9a..99999dbdfa 100644 --- a/core/modules/simpletest/src/WebTestBase.php +++ b/core/modules/simpletest/src/WebTestBase.php @@ -390,69 +390,6 @@ protected function setUp() { } /** - * Returns the parameters that will be used when Simpletest installs Drupal. - * - * @see install_drupal() - * @see install_state_defaults() - * - * @return array - * Array of parameters for use in install_drupal(). - */ - protected function installParameters() { - $connection_info = Database::getConnectionInfo(); - $driver = $connection_info['default']['driver']; - $connection_info['default']['prefix'] = $connection_info['default']['prefix']['default']; - unset($connection_info['default']['driver']); - unset($connection_info['default']['namespace']); - unset($connection_info['default']['pdo']); - unset($connection_info['default']['init_commands']); - // Remove database connection info that is not used by SQLite. - if ($driver == 'sqlite') { - unset($connection_info['default']['username']); - unset($connection_info['default']['password']); - unset($connection_info['default']['host']); - unset($connection_info['default']['port']); - } - $parameters = [ - 'interactive' => FALSE, - 'parameters' => [ - 'profile' => $this->profile, - 'langcode' => 'en', - ], - 'forms' => [ - 'install_settings_form' => [ - 'driver' => $driver, - $driver => $connection_info['default'], - ], - 'install_configure_form' => [ - 'site_name' => 'Drupal', - 'site_mail' => 'simpletest@example.com', - 'account' => [ - 'name' => $this->rootUser->name, - 'mail' => $this->rootUser->getEmail(), - 'pass' => [ - 'pass1' => $this->rootUser->pass_raw, - 'pass2' => $this->rootUser->pass_raw, - ], - ], - // \Drupal\Core\Render\Element\Checkboxes::valueCallback() requires - // NULL instead of FALSE values for programmatic form submissions to - // disable a checkbox. - 'enable_update_status_module' => NULL, - 'enable_update_status_emails' => NULL, - ], - ], - ]; - - // If we only have one db driver available, we cannot set the driver. - include_once DRUPAL_ROOT . '/core/includes/install.inc'; - if (count($this->getDatabaseTypes()) == 1) { - unset($parameters['forms']['install_settings_form']['driver']); - } - return $parameters; - } - - /** * Preserve the original batch, and instantiate the test batch. */ protected function setBatch() { @@ -480,21 +417,6 @@ protected function restoreBatch() { } /** - * Returns all supported database driver installer objects. - * - * This wraps drupal_get_database_types() for use without a current container. - * - * @return \Drupal\Core\Database\Install\Tasks[] - * An array of available database driver installer objects. - */ - protected function getDatabaseTypes() { - \Drupal::setContainer($this->originalContainer); - $database_types = drupal_get_database_types(); - \Drupal::unsetContainer(); - return $database_types; - } - - /** * Queues custom translations to be written to settings.php. * * Use WebTestBase::writeCustomTranslations() to apply and write the queued diff --git a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php new file mode 100644 index 0000000000..e963547ec7 --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php @@ -0,0 +1,452 @@ +databaseDumpFiles variable to the + * database dump files, and then call parent::setUp() to run the base setUp() + * method in this class. + * - In your test method, call $this->runUpdates() to run the necessary updates, + * and then use test assertions to verify that the result is what you expect. + * - In order to test both with a "bare" database dump as well as with a + * database dump filled with content, extend your update path test class with + * a new test class that overrides the bare database dump. Refer to + * UpdatePathTestBaseFilledTest for an example. + * + * @ingroup update_api + * + * @see hook_update_N() + */ +abstract class UpdatePathTestBase extends BrowserTestBase { + + use SchemaCheckTestTrait; + + /** + * Modules to enable after the database is loaded. + */ + protected static $modules = []; + + /** + * The file path(s) to the dumped database(s) to load into the child site. + * + * The file system/tests/fixtures/update/drupal-8.bare.standard.php.gz is + * normally included first -- this sets up the base database from a bare + * standard Drupal installation. + * + * The file system/tests/fixtures/update/drupal-8.filled.standard.php.gz + * can also be used in case we want to test with a database filled with + * content, and with all core modules enabled. + * + * @var array + */ + protected $databaseDumpFiles = []; + + /** + * The install profile used in the database dump file. + * + * @var string + */ + protected $installProfile = 'standard'; + + /** + * Flag that indicates whether the child site has been updated. + * + * @var bool + */ + protected $upgradedSite = FALSE; + + /** + * Array of errors triggered during the update process. + * + * @var array + */ + protected $upgradeErrors = []; + + /** + * Array of modules loaded when the test starts. + * + * @var array + */ + protected $loadedModules = []; + + /** + * Flag to indicate whether zlib is installed or not. + * + * @var bool + */ + protected $zlibInstalled = TRUE; + + /** + * Flag to indicate whether there are pending updates or not. + * + * @var bool + */ + protected $pendingUpdates = TRUE; + + /** + * The update URL. + * + * @var string + */ + protected $updateUrl; + + /** + * Disable strict config schema checking. + * + * The schema is verified at the end of running the update. + * + * @var bool + */ + protected $strictConfigSchema = FALSE; + + /** + * Fail the test if there are failed updates. + * + * @var bool + */ + protected $checkFailedUpdates = TRUE; + + /** + * Constructs an UpdatePathTestCase object. + * + * @param $test_id + * (optional) The ID of the test. Tests with the same id are reported + * together. + */ + public function __construct($test_id = NULL) { + parent::__construct($test_id); + $this->zlibInstalled = function_exists('gzopen'); + } + + /** + * Overrides WebTestBase::setUp() for update testing. + * + * The main difference in this method is that rather than performing the + * installation via the installer, a database is loaded. Additional work is + * then needed to set various things such as the config directories and the + * container that would normally be done via the installer. + */ + protected function setUp() { + $request = Request::createFromGlobals(); + + // Boot up Drupal into a state where calling the database API is possible. + // This is used to initialize the database system, so we can load the dump + // files. + $autoloader = require $this->root . '/autoload.php'; + $kernel = TestRunnerKernel::createFromRequest($request, $autoloader); + $kernel->loadLegacyIncludes(); + + $this->changeDatabasePrefix(); + $this->runDbTasks(); + // Allow classes to set database dump files. + $this->setDatabaseDumpFiles(); + + // 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 must be set here rather than in + // self::__construct() or the old URL generator will leak additional test + // sites. + $this->updateUrl = Url::fromRoute('system.db_update'); + + $this->setupBaseUrl(); + + // Install Drupal test site. + $this->prepareEnvironment(); + $this->installDrupal(); + + // Add the config directories to settings.php. + drupal_install_config_directories(); + + // Restore the original Simpletest batch. + // $this->restoreBatch(); + + // Set the container. parent::rebuildAll() would normally do this, but this + // not safe to do here, because the database has not been updated yet. + $this->container = \Drupal::getContainer(); + + $this->replaceUser1(); + + require_once \Drupal::root() . '/core/includes/update.inc'; + + // Setup Mink. + $session = $this->initMink(); + + $cookies = $this->extractCookiesFromRequest(\Drupal::request()); + foreach ($cookies as $cookie_name => $values) { + foreach ($values as $value) { + $session->setCookie($cookie_name, $value); + } + } + + // Creates the directory to store browser output in if a file to write + // URLs to has been created by \Drupal\Tests\Listeners\HtmlOutputPrinter. + $browser_output_file = getenv('BROWSERTEST_OUTPUT_FILE'); + $this->htmlOutputEnabled = is_file($browser_output_file); + if ($this->htmlOutputEnabled) { + $this->htmlOutputFile = $browser_output_file; + $this->htmlOutputClassName = str_replace("\\", "_", get_called_class()); + $this->htmlOutputDirectory = DRUPAL_ROOT . '/sites/simpletest/browser_output'; + if (file_prepare_directory($this->htmlOutputDirectory, FILE_CREATE_DIRECTORY) && !file_exists($this->htmlOutputDirectory . '/.htaccess')) { + file_put_contents($this->htmlOutputDirectory . '/.htaccess', "\nExpiresActive Off\n\n"); + } + $this->htmlOutputCounterStorage = $this->htmlOutputDirectory . '/' . $this->htmlOutputClassName . '.counter'; + $this->htmlOutputTestId = str_replace('sites/simpletest/', '', $this->siteDirectory); + if (is_file($this->htmlOutputCounterStorage)) { + $this->htmlOutputCounter = max(1, (int) file_get_contents($this->htmlOutputCounterStorage)) + 1; + } + } + } + + /** + * {@inheritdoc} + */ + public function installDrupal() { + $this->initUserSession(); + $this->prepareSettings(); + $this->doInstall(); + $this->initSettings(); + + $request = Request::createFromGlobals(); + $container = $this->initKernel($request); + $this->initConfig($container); + } + + /** + * {@inheritdoc} + */ + protected function doInstall() { + $this->runDbTasks(); + // Allow classes to set database dump files. + $this->setDatabaseDumpFiles(); + + // Load the database(s). + foreach ($this->databaseDumpFiles as $file) { + if (substr($file, -3) == '.gz') { + $file = "compress.zlib://$file"; + } + require $file; + } + } + + /** + * {@inheritdoc} + */ + protected function initMink() { + $driver = $this->getDefaultDriverInstance(); + + if ($driver instanceof GoutteDriver) { + // Turn off curl timeout. Having a timeout is not a problem in a normal + // test running, but it is a problem when debugging. Also, disable SSL + // peer verification so that testing under HTTPS always works. + /** @var \GuzzleHttp\Client $client */ + $client = $this->container->get('http_client_factory')->fromOptions([ + 'timeout' => NULL, + 'verify' => FALSE, + ]); + + // Inject a Guzzle middleware to generate debug output for every request + // performed in the test. + $handler_stack = $client->getConfig('handler'); + $handler_stack->push($this->getResponseLogHandler()); + + $driver->getClient()->setClient($client); + } + + $selectors_handler = new SelectorsHandler([ + 'hidden_field_selector' => new HiddenFieldSelector() + ]); + $session = new Session($driver, $selectors_handler); + $this->mink = new Mink(); + $this->mink->registerSession('default', $session); + $this->mink->setDefaultSessionName('default'); + $this->registerSessions(); + + return $session; + } + + /** + * Set database dump files to be used. + */ + abstract protected function setDatabaseDumpFiles(); + + /** + * Add settings that are missed since the installer isn't run. + */ + protected function prepareSettings() { + parent::prepareSettings(); + + // Remember the profile which was used. + $settings['settings']['install_profile'] = (object) [ + 'value' => $this->installProfile, + 'required' => TRUE, + ]; + // Generate a hash salt. + $settings['settings']['hash_salt'] = (object) [ + 'value' => Crypt::randomBytesBase64(55), + 'required' => TRUE, + ]; + + // Since the installer isn't run, add the database settings here too. + $settings['databases']['default'] = (object) [ + 'value' => Database::getConnectionInfo(), + 'required' => TRUE, + ]; + + $this->writeSettings($settings); + } + + /** + * Helper function to run pending database updates. + */ + protected function runUpdates() { + if (!$this->zlibInstalled) { + $this->fail('Missing zlib requirement for update tests.'); + return FALSE; + } + // The site might be broken at the time so logging in using the UI might + // not work, so we use the API itself. + drupal_rewrite_settings(['settings' => ['update_free_access' => (object) [ + 'value' => TRUE, + 'required' => TRUE, + ]]]); + + $this->drupalGet($this->updateUrl); + $this->clickLink(t('Continue')); + + $this->doSelectionTest(); + // Run the update hooks. + $this->clickLink(t('Apply pending updates')); + $this->checkForMetaRefresh(); + + // Ensure there are no failed updates. + if ($this->checkFailedUpdates) { + $this->assertNoRaw('' . t('Failed:') . ''); + + // Ensure that there are no pending updates. + foreach (['update', 'post_update'] as $update_type) { + switch ($update_type) { + case 'update': + $all_updates = update_get_update_list(); + break; + case 'post_update': + $all_updates = \Drupal::service('update.post_update_registry')->getPendingUpdateInformation(); + break; + } + foreach ($all_updates as $module => $updates) { + if (!empty($updates['pending'])) { + foreach (array_keys($updates['pending']) as $update_name) { + $this->fail("The $update_name() update function from the $module module did not run."); + } + } + } + } + // Reset the static cache of drupal_get_installed_schema_version() so that + // more complex update path testing works. + drupal_static_reset('drupal_get_installed_schema_version'); + + // The config schema can be incorrect while the update functions are being + // executed. But once the update has been completed, it needs to be valid + // again. Assert the schema of all configuration objects now. + $names = $this->container->get('config.storage')->listAll(); + /** @var \Drupal\Core\Config\TypedConfigManagerInterface $typed_config */ + $typed_config = $this->container->get('config.typed'); + $typed_config->clearCachedDefinitions(); + foreach ($names as $name) { + $config = $this->config($name); + $this->assertConfigSchema($typed_config, $name, $config->get()); + } + + // Ensure that the update hooks updated all entity schema. + $needs_updates = \Drupal::entityDefinitionUpdateManager()->needsUpdates(); + $this->assertFalse($needs_updates, 'After all updates ran, entity schema is up to date.'); + if ($needs_updates) { + foreach (\Drupal::entityDefinitionUpdateManager() + ->getChangeSummary() as $entity_type_id => $summary) { + foreach ($summary as $message) { + $this->fail($message); + } + } + } + } + } + + /** + * Runs the install database tasks for the driver used by the test runner. + */ + protected function runDbTasks() { + // Create a minimal container so that t() works. + // @see install_begin_request() + $container = new ContainerBuilder(); + $container->setParameter('language.default_values', Language::$defaultValues); + $container + ->register('language.default', 'Drupal\Core\Language\LanguageDefault') + ->addArgument('%language.default_values%'); + $container + ->register('string_translation', 'Drupal\Core\StringTranslation\TranslationManager') + ->addArgument(new Reference('language.default')); + \Drupal::setContainer($container); + + require_once __DIR__ . '/../../../../includes/install.inc'; + $connection = Database::getConnection(); + $errors = db_installer_object($connection->driver())->runTasks(); + if (!empty($errors)) { + $this->fail('Failed to run installer database tasks: ' . implode(', ', $errors)); + } + } + + /** + * Replace User 1 with the user created here. + */ + protected function replaceUser1() { + /** @var \Drupal\user\UserInterface $account */ + // @todo: Saving the account before the update is problematic. + // https://www.drupal.org/node/2560237 + $account = User::load(1); + $account->setPassword($this->rootUser->pass_raw); + $account->setEmail($this->rootUser->getEmail()); + $account->setUsername($this->rootUser->getUsername()); + $account->save(); + } + + /** + * Tests the selection page. + */ + protected function doSelectionTest() { + // No-op. Tests wishing to do test the selection page or the general + // update.php environment before running update.php can override this method + // and implement their required tests. + } + +} diff --git a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php new file mode 100644 index 0000000000..911d523a0e --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php @@ -0,0 +1,97 @@ +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', + ]; + } + + /** + * Tests that the database was properly loaded. + */ + public 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])); + } + + // Ensure that all {router} entries can be unserialized. If they cannot be + // unserialized a notice will be thrown by PHP. + + $result = \Drupal::database()->query("SELECT name, route from {router}")->fetchAllKeyed(0, 1); + // For the purpose of fetching the notices and displaying more helpful error + // messages, let's override the error handler temporarily. + set_error_handler(function ($severity, $message, $filename, $lineno) { + throw new \ErrorException($message, 0, $severity, $filename, $lineno); + }); + foreach ($result as $route_name => $route) { + try { + unserialize($route); + } + catch (\Exception $e) { + $this->fail(sprintf('Error "%s" while unserializing route %s', $e->getMessage(), Html::escape($route_name))); + } + } + restore_error_handler(); + + // Before accessing the site we need to run updates first or the site might + // be broken. + $this->runUpdates(); + $this->assertEqual(\Drupal::config('system.site')->get('name'), 'Site-Install'); + $this->drupalGet(''); + $this->assertText('Site-Install'); + + // Ensure that the database tasks have been run during set up. Neither MySQL + // nor SQLite make changes that are testable. + $database = $this->container->get('database'); + if ($database->driver() == 'pgsql') { + $this->assertEqual('on', $database->query("SHOW standard_conforming_strings")->fetchField()); + $this->assertEqual('escape', $database->query("SHOW bytea_output")->fetchField()); + } + } + + /** + * Test that updates are properly run. + */ + public function testUpdateHookN() { + // Increment the schema version. + \Drupal::state()->set('update_test_schema_version', 8001); + $this->runUpdates(); + + $select = \Drupal::database()->select('watchdog'); + $select->orderBy('wid', 'DESC'); + $select->range(0, 5); + $select->fields('watchdog', ['message']); + + $container_cannot_be_saved_messages = array_filter(iterator_to_array($select->execute()), function($row) { + return strpos($row->message, 'Container cannot be saved to cache.') !== FALSE; + }); + $this->assertEqual([], $container_cannot_be_saved_messages); + + // 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/tests/Drupal/Tests/BrowserTestBase.php b/core/tests/Drupal/Tests/BrowserTestBase.php index bd50c23b48..928152fe8c 100644 --- a/core/tests/Drupal/Tests/BrowserTestBase.php +++ b/core/tests/Drupal/Tests/BrowserTestBase.php @@ -14,10 +14,7 @@ use Drupal\Core\Database\Database; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\AnonymousUserSession; -use Drupal\Core\Site\Settings; -use Drupal\Core\StreamWrapper\StreamWrapperInterface; use Drupal\Core\Test\FunctionalTestSetupTrait; -use Drupal\Core\Test\TestRunnerKernel; use Drupal\Core\Test\TestSetupTrait; use Drupal\Core\Url; use Drupal\Core\Utility\Error; @@ -31,7 +28,6 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Symfony\Component\CssSelector\CssSelectorConverter; -use Symfony\Component\HttpFoundation\Request; /** * Provides a test case for functional Drupal tests. @@ -262,6 +258,32 @@ protected $metaRefreshCount = 0; /** + * The app root. + * + * @var string + */ + protected $root; + + /** + * The original container. + * + * Move this to \Drupal\Core\Test\FunctionalTestSetupTrait once TestBase no + * longer provides the same value. + * + * @var \Symfony\Component\DependencyInjection\ContainerInterface + */ + protected $originalContainer; + + /** + * {@inheritdoc} + */ + public function __construct($name = NULL, array $data = array(), $dataName = '') { + parent::__construct($name, $data, $dataName); + + $this->root = dirname(dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__)))); + } + + /** * Initializes Mink sessions. */ protected function initMink() { @@ -400,43 +422,9 @@ protected function registerSessions() {} * {@inheritdoc} */ protected function setUp() { - global $base_url; parent::setUp(); - // Get and set the domain of the environment we are running our test - // coverage against. - $base_url = getenv('SIMPLETEST_BASE_URL'); - if (!$base_url) { - throw new \Exception( - 'You must provide a SIMPLETEST_BASE_URL environment variable to run some PHPUnit based functional tests.' - ); - } - - // Setup $_SERVER variable. - $parsed_url = parse_url($base_url); - $host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''); - $path = isset($parsed_url['path']) ? rtrim(rtrim($parsed_url['path']), '/') : ''; - $port = isset($parsed_url['port']) ? $parsed_url['port'] : 80; - - $this->baseUrl = $base_url; - - // If the passed URL schema is 'https' then setup the $_SERVER variables - // properly so that testing will run under HTTPS. - if ($parsed_url['scheme'] === 'https') { - $_SERVER['HTTPS'] = 'on'; - } - $_SERVER['HTTP_HOST'] = $host; - $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; - $_SERVER['SERVER_ADDR'] = '127.0.0.1'; - $_SERVER['SERVER_PORT'] = $port; - $_SERVER['SERVER_SOFTWARE'] = NULL; - $_SERVER['SERVER_NAME'] = 'localhost'; - $_SERVER['REQUEST_URI'] = $path . '/'; - $_SERVER['REQUEST_METHOD'] = 'GET'; - $_SERVER['SCRIPT_NAME'] = $path . '/index.php'; - $_SERVER['SCRIPT_FILENAME'] = $path . '/index.php'; - $_SERVER['PHP_SELF'] = $path . '/index.php'; - $_SERVER['HTTP_USER_AGENT'] = 'Drupal command line'; + $this->setupBaseUrl(); // Install Drupal test site. $this->prepareEnvironment(); @@ -980,134 +968,6 @@ public function installDrupal() { } /** - * Returns the parameters that will be used when Simpletest installs Drupal. - * - * @see install_drupal() - * @see install_state_defaults() - */ - protected function installParameters() { - $connection_info = Database::getConnectionInfo(); - $driver = $connection_info['default']['driver']; - $connection_info['default']['prefix'] = $connection_info['default']['prefix']['default']; - unset($connection_info['default']['driver']); - unset($connection_info['default']['namespace']); - unset($connection_info['default']['pdo']); - unset($connection_info['default']['init_commands']); - $parameters = [ - 'interactive' => FALSE, - 'parameters' => [ - 'profile' => $this->profile, - 'langcode' => 'en', - ], - 'forms' => [ - 'install_settings_form' => [ - 'driver' => $driver, - $driver => $connection_info['default'], - ], - 'install_configure_form' => [ - 'site_name' => 'Drupal', - 'site_mail' => 'simpletest@example.com', - 'account' => [ - 'name' => $this->rootUser->name, - 'mail' => $this->rootUser->getEmail(), - 'pass' => [ - 'pass1' => $this->rootUser->pass_raw, - 'pass2' => $this->rootUser->pass_raw, - ], - ], - // form_type_checkboxes_value() requires NULL instead of FALSE values - // for programmatic form submissions to disable a checkbox. - 'enable_update_status_module' => NULL, - 'enable_update_status_emails' => NULL, - ], - ], - ]; - return $parameters; - } - - /** - * Prepares the current environment for running the test. - * - * Also sets up new resources for the testing environment, such as the public - * filesystem and configuration directories. - * - * This method is private as it must only be called once by - * BrowserTestBase::setUp() (multiple invocations for the same test would have - * unpredictable consequences) and it must not be callable or overridable by - * test classes. - */ - protected function prepareEnvironment() { - // Bootstrap Drupal so we can use Drupal's built in functions. - $this->classLoader = require __DIR__ . '/../../../../autoload.php'; - $request = Request::createFromGlobals(); - $kernel = TestRunnerKernel::createFromRequest($request, $this->classLoader); - // TestRunnerKernel expects the working directory to be DRUPAL_ROOT. - chdir(DRUPAL_ROOT); - $kernel->prepareLegacyRequest($request); - $this->prepareDatabasePrefix(); - - $this->originalSite = $kernel->findSitePath($request); - - // Create test directory ahead of installation so fatal errors and debug - // information can be logged during installation process. - file_prepare_directory($this->siteDirectory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); - - // Prepare filesystem directory paths. - $this->publicFilesDirectory = $this->siteDirectory . '/files'; - $this->privateFilesDirectory = $this->siteDirectory . '/private'; - $this->tempFilesDirectory = $this->siteDirectory . '/temp'; - $this->translationFilesDirectory = $this->siteDirectory . '/translations'; - - // Ensure the configImporter is refreshed for each test. - $this->configImporter = NULL; - - // Unregister all custom stream wrappers of the parent site. - $wrappers = \Drupal::service('stream_wrapper_manager')->getWrappers(StreamWrapperInterface::ALL); - foreach ($wrappers as $scheme => $info) { - stream_wrapper_unregister($scheme); - } - - // Reset statics. - drupal_static_reset(); - - // Ensure there is no service container. - $this->container = NULL; - \Drupal::unsetContainer(); - - // Unset globals. - unset($GLOBALS['config_directories']); - unset($GLOBALS['config']); - unset($GLOBALS['conf']); - - // Log fatal errors. - ini_set('log_errors', 1); - ini_set('error_log', DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log'); - - // Change the database prefix. - $this->changeDatabasePrefix(); - - // After preparing the environment and changing the database prefix, we are - // in a valid test environment. - drupal_valid_test_ua($this->databasePrefix); - - // Reset settings. - new Settings([ - // For performance, simply use the database prefix as hash salt. - 'hash_salt' => $this->databasePrefix, - ]); - - drupal_set_time_limit($this->timeLimit); - - // Save and clean the shutdown callbacks array because it is static cached - // and will be changed by the test run. Otherwise it will contain callbacks - // from both environments and the testing environment will try to call the - // handlers defined by the original one. - $callbacks = &drupal_register_shutdown_function(); - $this->originalShutdownCallbacks = $callbacks; - $callbacks = []; - } - - /** * Returns whether a given user account is logged in. * * @param \Drupal\Core\Session\AccountInterface $account