diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index d92e27c6b1bb1a0cfc8c9c3e12bfb3a9b15315c4..b2924892b02b9b6aca8b52d262cdedc61a4b0f87 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -240,15 +240,15 @@ * @see default.settings.php */ function conf_path($require_settings = TRUE, $reset = FALSE) { - static $conf_path; + $conf_path = &drupal_static(__FUNCTION__, ''); - if (isset($conf_path) && !$reset) { + if ($conf_path && !$reset) { return $conf_path; } // Check for a simpletest override. - if ($test_prefix = drupal_valid_test_ua()) { - $conf_path = 'sites/simpletest/' . substr($test_prefix, 10); + if ($simpletest_conf_path = _drupal_simpletest_conf_path()) { + $conf_path = $simpletest_conf_path; return $conf_path; } @@ -262,6 +262,50 @@ function conf_path($require_settings = TRUE, $reset = FALSE) { return $conf_path; } +/** + * Determines whether to use an overridden value for conf_path(). + * + * Simpletest may provide a secondary, test-specific settings.php file to load + * after the primary one used by the parent site and override its variables. + * - If the child settings.php does not override $conf_path, then this function + * returns FALSE and conf_path() returns the directory of the primary + * settings.php. + * - If the child settings.php does override $conf_path, then + * _drupal_load_test_overrides() sets the 'simpletest_conf_path' setting, and + * this function returns that to conf_path(), causing installations and + * upgrades to act on that one. + * + * @return string|false + * The overridden $conf_path, or FALSE if the $conf_path should not currently + * be overridden. + * + * @see conf_path() + * @see _drupal_load_test_overrides() + */ +function _drupal_simpletest_conf_path() { + // Ensure that the settings object is available. conf_path() is called once + // before the Settings class is included, and at that point it should still + // load the primary $conf_path. See drupal_settings_initialize(). + if (!class_exists('Drupal\Component\Utility\Settings', FALSE)) { + return FALSE; + } + + // If no $simpletest_conf_path is set, use the normal $conf_path. + if (!($simpletest_conf_path = settings()->get('simpletest_conf_path'))) { + return FALSE; + } + + // Ensure that this is actually a simpletest request. We can't check this + // before settings.php is loaded. + if (!drupal_valid_test_ua()) { + return FALSE; + } + + // When the $simpletest_conf_path is set in a valid test request, + // return that path. + return $simpletest_conf_path; +} + /** * Finds the appropriate configuration directory for a given host and path. * @@ -490,32 +534,20 @@ function drupal_valid_http_host($host) { * Sets the base URL, cookie domain, and session name from configuration. */ function drupal_settings_initialize() { + global $base_url, $base_path, $base_root, $script_path; + // Export these settings.php variables to the global namespace. - global $base_url, $databases, $cookie_domain, $drupal_hash_salt, $config_directories, $config; + global $databases, $cookie_domain, $db_prefix, $drupal_hash_salt, $base_secure_url, $base_insecure_url, $config_directories, $config; $settings = array(); $config = array(); // Make conf_path() available as local variable in settings.php. $conf_path = conf_path(); if (is_readable(DRUPAL_ROOT . '/' . $conf_path . '/settings.php')) { - require DRUPAL_ROOT . '/' . $conf_path . '/settings.php'; + include_once DRUPAL_ROOT . '/' . $conf_path . '/settings.php'; } // Initialize Settings. new Settings($settings); -} - -/** - * Initializes global request variables. - * - * @todo D8: Eliminate this entirely in favor of Request object. - */ -function _drupal_request_initialize() { - // Provided by settings.php. - // @see drupal_settings_initialize() - global $base_url, $cookie_domain; - // Set and derived from $base_url by this function. - global $base_path, $base_root, $script_path; - global $base_secure_url, $base_insecure_url; $is_https = isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on'; @@ -1721,27 +1753,11 @@ function _drupal_exception_handler($exception) { */ function _drupal_bootstrap_configuration() { drupal_environment_initialize(); - - // Indicate that code is operating in a test child site. - if ($test_prefix = drupal_valid_test_ua()) { - // Only code that interfaces directly with tests should rely on this - // constant; e.g., the error/exception handler conditionally adds further - // error information into HTTP response headers that are consumed by - // Simpletest's internal browser. - define('DRUPAL_TEST_IN_CHILD_SITE', TRUE); - - // Log fatal errors to the test site directory. - ini_set('log_errors', 1); - ini_set('error_log', DRUPAL_ROOT . '/sites/simpletest/' . substr($test_prefix, 10) . '/error.log'); - } - else { - // Ensure that no other code defines this. - define('DRUPAL_TEST_IN_CHILD_SITE', FALSE); - } - // Initialize the configuration, including variables from settings.php. drupal_settings_initialize(); - _drupal_request_initialize(); + + // Make sure we are using the test database prefix in child Drupal sites. + _drupal_initialize_db_test_prefix(); // Activate the class loader. drupal_classloader(); @@ -1829,6 +1845,39 @@ function _drupal_bootstrap_page_cache() { } } +/** + * In a test environment, get the test db prefix and set it in $databases. + */ +function _drupal_initialize_db_test_prefix() { + // The user agent header is used to pass a database prefix in the request when + // running tests. However, for security reasons, it is imperative that we + // validate we ourselves made the request. + if ($test_prefix = drupal_valid_test_ua()) { + // Set the test run id for use in other parts of Drupal. + $test_info = &$GLOBALS['drupal_test_info']; + $test_info['test_run_id'] = $test_prefix; + $test_info['in_child_site'] = TRUE; + + foreach ($GLOBALS['databases']['default'] as &$value) { + // Extract the current default database prefix. + if (!isset($value['prefix'])) { + $current_prefix = ''; + } + elseif (is_array($value['prefix'])) { + $current_prefix = $value['prefix']['default']; + } + else { + $current_prefix = $value['prefix']; + } + + // Remove the current database prefix and replace it by our own. + $value['prefix'] = array( + 'default' => $current_prefix . $test_prefix, + ); + } + } +} + /** * Returns the current bootstrap phase for this Drupal process. * @@ -1943,9 +1992,10 @@ function module_hook($module, $hook) { * Returns the test prefix if this is an internal request from SimpleTest. * * @param string $new_prefix - * Internal use only. A new prefix to be stored. + * Internal use only. A new prefix to be stored. Passed in by tests that use + * the test runner from within a test. * - * @return string|FALSE + * @return * Either the simpletest prefix (the string "simpletest" followed by any * number of digits) or FALSE if the user agent does not contain a valid * HMAC and timestamp. @@ -1959,68 +2009,82 @@ function drupal_valid_test_ua($new_prefix = NULL) { if (isset($test_prefix)) { return $test_prefix; } - // Unless the below User-Agent and HMAC validation succeeds, we are not in - // a test environment. - $test_prefix = FALSE; - // Perform a basic check on the User-Agent HTTP request header first. Any - // inbound request that uses the simpletest UA header needs to be validated. if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^(simpletest\d+);(.+);(.+);(.+)$/", $_SERVER['HTTP_USER_AGENT'], $matches)) { list(, $prefix, $time, $salt, $hmac) = $matches; $check_string = $prefix . ';' . $time . ';' . $salt; - // Read the hash salt prepared by drupal_generate_test_ua(). - // This function is called before settings.php is read and Drupal's error - // handlers are set up. While Drupal's error handling may be properly - // configured on production sites, the server's PHP error_reporting may not. - // Ensure that no information leaks on production sites. - $key_file = DRUPAL_ROOT . '/sites/simpletest/' . substr($prefix, 10) . '/.htkey'; - if (!is_readable($key_file)) { - header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden'); - exit; - } - $private_key = file_get_contents($key_file); - // The file properties add more entropy not easily accessible to others. - $key = $private_key . filectime(__FILE__) . fileinode(__FILE__); + // We use the salt from settings.php to make the HMAC key, since + // the database is not yet initialized and we can't access the configuration + // system. The file properties add more entropy not easily accessible to + // others. + $key = drupal_get_hash_salt() . filectime(__FILE__) . fileinode(__FILE__); $time_diff = REQUEST_TIME - $time; - $test_hmac = Crypt::hmacBase64($check_string, $key); + // We can't use Crypt::hmacBase64() yet because this can be called in very + // early bootstrap when autoloader has not been initialized yet. + $test_hmac = base64_encode(hash_hmac('sha256', $check_string, $key, TRUE)); + $test_hmac = strtr($test_hmac, array('+' => '-', '/' => '_', '=' => '')); // Since we are making a local request a 5 second time window is allowed, // and the HMAC must match. - if ($time_diff >= 0 && $time_diff <= 5 && $hmac === $test_hmac) { + if ($time_diff >= 0 && $time_diff <= 5 && $hmac == $test_hmac) { $test_prefix = $prefix; + _drupal_load_test_overrides($test_prefix); + return $test_prefix; } } + + $test_prefix = FALSE; return $test_prefix; } +/** + * Overrides low-level and environment-specific configuration. + * + * Very strictly for internal use only. + * + * Loads settings.php from the simpletest public files directory. These files + * can change the global $config_directories, the return value of conf_path(), + * settings(), and $config overrides. + * + * @param string $test_prefix + * The simpletest prefix. + */ +function _drupal_load_test_overrides($test_prefix) { + global $config_directories, $config; + + // Do not use the parent site's config directories. Use only the child site's. + // @see \Drupal\simpletest\TestBase::prepareConfigDirectories() + $path_prefix = 'simpletest/' . substr($test_prefix, 10); + $config_directories = array(); + foreach (array(CONFIG_ACTIVE_DIRECTORY, CONFIG_STAGING_DIRECTORY) as $type) { + $config_directories[$type] = conf_path() . '/files/' . $path_prefix . '/config_' . $type; + } + + // Check for and load a settings.php file in the simpletest files directory. + $filename = conf_path() . '/files/' . $path_prefix . '/settings.php'; + if (file_exists($filename)) { + $settings = settings()->getAll(); + $conf_path = &drupal_static('conf_path'); + // This can override $config, $conf_path, $settings, and $config_directories. + include $filename; + // Keep the overriden $conf_path alive across drupal_static_reset() calls. + // @see conf_path() + $settings['simpletest_conf_path'] = $conf_path; + new Settings($settings); + } +} + /** * Generates a user agent string with a HMAC and timestamp for simpletest. */ function drupal_generate_test_ua($prefix) { - static $key, $last_prefix; - - if (!isset($key) || $last_prefix != $prefix) { - $last_prefix = $prefix; - $key_file = DRUPAL_ROOT . '/sites/simpletest/' . substr($prefix, 10) . '/.htkey'; - // When issuing an outbound HTTP client request from within an inbound test - // request, then the outbound request has to use the same User-Agent header - // as the inbound request. A newly generated private key for the same test - // prefix would invalidate all subsequent inbound requests. - // @see \Drupal\Core\Http\Plugin\SimpletestHttpRequestSubscriber - if (DRUPAL_TEST_IN_CHILD_SITE && $parent_prefix = drupal_valid_test_ua()) { - if ($parent_prefix != $prefix) { - throw new \RuntimeException("Malformed User-Agent: Expected '$parent_prefix' but got '$prefix'."); - } - // If the file is not readable, a PHP warning is expected in this case. - $private_key = file_get_contents($key_file); - } - else { - // Generate and save a new hash salt for a test run. - // Consumed by drupal_valid_test_ua() before settings.php is loaded. - $private_key = Crypt::randomStringHashed(55); - file_put_contents($key_file, $private_key); - } - // The file properties add more entropy not easily accessible to others. - $key = $private_key . filectime(__FILE__) . fileinode(__FILE__); + static $key; + + if (!isset($key)) { + // We use the salt from settings.php to make the HMAC key, since + // the database is not yet initialized and we can't access the configuration + // system. The file properties add more entropy not easily accessible to + // others. + $key = drupal_get_hash_salt() . filectime(__FILE__) . fileinode(__FILE__); } // Generate a moderately secure HMAC based on the database credentials. $salt = uniqid('', TRUE); diff --git a/core/includes/common.inc b/core/includes/common.inc index 170e9411601220f8e0ed7f4fe0e5b45251adb7f4..a566bc527abe6ccaf969405ac1d6b7fa971d7ca9 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -3071,6 +3071,14 @@ function _drupal_bootstrap_code() { // Make sure all stream wrappers are registered. file_get_stream_wrappers(); + // Now that stream wrappers are registered, log fatal errors from a simpletest + // child site to a test specific file directory. + $test_info = &$GLOBALS['drupal_test_info']; + if (!empty($test_info['in_child_site'])) { + ini_set('log_errors', 1); + ini_set('error_log', 'public://error.log'); + } + // Set the allowed protocols once we have the config available. $allowed_protocols = \Drupal::config('system.filter')->get('protocols'); if (!isset($allowed_protocols)) { diff --git a/core/includes/errors.inc b/core/includes/errors.inc index 208a6791af9bb3d566c139fc5d20e06765f39119..586effad13d35184bec17e6499eddd10cec3c7a3 100644 --- a/core/includes/errors.inc +++ b/core/includes/errors.inc @@ -141,7 +141,8 @@ function _drupal_log_error($error, $fatal = FALSE) { // When running inside the testing framework, we relay the errors // to the tested site by the way of HTTP headers. - if (DRUPAL_TEST_IN_CHILD_SITE && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) { + $test_info = &$GLOBALS['drupal_test_info']; + if (!empty($test_info['in_child_site']) && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) { // $number does not use drupal_static as it should not be reset // as it uniquely identifies each PHP error. static $number = 0; diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 9b3d7a0ba1d7d9d4db0e42229733a6f04590f7c2..0b25e590a1e198abde87110680d0966a2ad96fa0 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -276,18 +276,19 @@ function install_begin_request(&$install_state) { // which will be used for installing Drupal. conf_path(FALSE); + drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION); + // If the hash salt leaks, it becomes possible to forge a valid testing user - // agent, install a new copy of Drupal, and take over the original site. - // The user agent header is used to pass a database prefix in the request when - // running tests. However, for security reasons, it is imperative that no - // installation be permitted using such a prefix. - if ($install_state['interactive'] && strpos($request->server->get('HTTP_USER_AGENT'), 'simpletest') !== FALSE && !drupal_valid_test_ua()) { + // agent, install a new copy of Drupal, and take over the original site. To + // avoid this yet allow for automated testing of the installer, make sure + // there is also a special test-specific settings.php overriding conf_path(). + // _drupal_load_test_overrides() sets the simpletest_conf_path in-memory + // setting in this case. + if ($install_state['interactive'] && drupal_valid_test_ua() && !settings()->get('simpletest_conf_path')) { header($request->server->get('SERVER_PROTOCOL') . ' 403 Forbidden'); exit; } - drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION); - // Ensure that procedural dependencies are loaded as early as possible, // since the error/exception handlers depend on them. require_once __DIR__ . '/../modules/system/system.install'; @@ -1079,6 +1080,7 @@ function install_verify_database_settings() { global $databases; if (!empty($databases)) { $database = $databases['default']['default']; + drupal_static_reset('conf_path'); $settings_file = './' . conf_path(FALSE) . '/settings.php'; $errors = install_database_errors($database, $settings_file); if (empty($errors)) { @@ -1101,6 +1103,7 @@ function install_verify_database_settings() { function install_settings_form($form, &$form_state, &$install_state) { global $databases; + drupal_static_reset('conf_path'); $conf_path = './' . conf_path(FALSE); $settings_file = $conf_path . '/settings.php'; $database = isset($databases['default']['default']) ? $databases['default']['default'] : array(); @@ -1162,6 +1165,12 @@ function install_settings_form($form, &$form_state, &$install_state) { function install_settings_form_validate($form, &$form_state) { $driver = $form_state['values']['driver']; $database = $form_state['values'][$driver]; + // When testing the interactive installer, copy the database password and + // the test prefix. + if ($test_prefix = drupal_valid_test_ua()) { + $database['prefix'] = $test_prefix; + $database['password'] = $GLOBALS['databases']['default']['default']['password']; + } $drivers = drupal_get_database_types(); $reflection = new \ReflectionClass($drivers[$driver]); $install_namespace = $reflection->getNamespaceName(); @@ -1228,14 +1237,33 @@ function install_settings_form_submit($form, &$form_state) { // Update global settings array and save. $settings = array(); $database = $form_state['storage']['database']; - $settings['databases']['default']['default'] = (object) array( - 'value' => $database, - 'required' => TRUE, - ); - $settings['drupal_hash_salt'] = (object) array( - 'value' => Crypt::randomStringHashed(55), - 'required' => TRUE, - ); + // Ideally, there is no difference between the code executed by the + // automated test browser and an ordinary browser. However, the database + // settings need a different format and also need to skip the password + // when testing. The hash salt also needs to be skipped because the original + // salt is used to verify the validity of the automated test browser. + // Because of these, there's a little difference in the code following but + // it is small and self-contained. + if ($test_prefix = drupal_valid_test_ua()) { + foreach ($form_state['storage']['database'] as $k => $v) { + if ($k != 'password') { + $settings['databases']['default']['default'][$k] = (object) array( + 'value' => $v, + 'required' => TRUE, + ); + } + } + } + else { + $settings['databases']['default']['default'] = (object) array( + 'value' => $database, + 'required' => TRUE, + ); + $settings['drupal_hash_salt'] = (object) array( + 'value' => Crypt::randomStringHashed(55), + 'required' => TRUE, + ); + } // Remember the profile which was used. $settings['settings'] = array( @@ -2353,7 +2381,7 @@ function install_check_requirements($install_state) { if (!$install_state['settings_verified']) { $readable = FALSE; $writable = FALSE; - $conf_path = './' . conf_path(FALSE); + $conf_path = './' . conf_path(FALSE, TRUE); $settings_file = $conf_path . '/settings.php'; $default_settings_file = './sites/default/default.settings.php'; $file = $conf_path; diff --git a/core/lib/Drupal/Core/Controller/ExceptionController.php b/core/lib/Drupal/Core/Controller/ExceptionController.php index c8da4bc38fd243aa7e65deb51b312967718ad02f..e33a00a8cd4009eaa72037efc064dbc71566771d 100644 --- a/core/lib/Drupal/Core/Controller/ExceptionController.php +++ b/core/lib/Drupal/Core/Controller/ExceptionController.php @@ -292,7 +292,8 @@ public function on500Html(FlattenException $exception, Request $request) { // When running inside the testing framework, we relay the errors // to the tested site by the way of HTTP headers. - if (DRUPAL_TEST_IN_CHILD_SITE && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) { + $test_info = &$GLOBALS['drupal_test_info']; + if (!empty($test_info['in_child_site']) && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) { // $number does not use drupal_static as it should not be reset // as it uniquely identifies each PHP error. static $number = 0; diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php index 97eab5441ac82358c60bb271b7f544086c2ee636..633edc6ca9ea5dbfe433730d98a993bbd8c33c5c 100644 --- a/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php @@ -49,7 +49,7 @@ public function getFormOptions(array $database) { // Make the text more accurate for SQLite. $form['database']['#title'] = t('Database file'); $form['database']['#description'] = t('The absolute path to the file where @drupal data will be stored. This must be writable by the web server and should exist outside of the web root.', array('@drupal' => drupal_install_profile_distribution_name())); - $default_database = conf_path(FALSE) . '/files/.ht.sqlite'; + $default_database = conf_path(FALSE, TRUE) . '/files/.ht.sqlite'; $form['database']['#default_value'] = empty($database['database']) ? $default_database : $database['database']; return $form; } diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index f9107febea68b59dddf20fed157c4d681ba03524..85fa7f817ee594907e4cc5b2f1f55766e19e1646 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -335,13 +335,20 @@ public function updateModules(array $module_list, array $module_filenames = arra } /** - * Returns the classname based on environment. + * Returns the classname based on environment and testing prefix. * * @return string * The class name. */ protected function getClassName() { $parts = array('service_container', $this->environment); + // Make sure to use a testing-specific container even in the parent site. + if (!empty($GLOBALS['drupal_test_info']['test_run_id'])) { + $parts[] = $GLOBALS['drupal_test_info']['test_run_id']; + } + elseif ($prefix = drupal_valid_test_ua()) { + $parts[] = $prefix; + } return implode('_', $parts); } diff --git a/core/lib/Drupal/Core/Http/Plugin/SimpletestHttpRequestSubscriber.php b/core/lib/Drupal/Core/Http/Plugin/SimpletestHttpRequestSubscriber.php index a8de4a89d209c471fd888eb0e14a262cf2e295ca..6f5bfd17696bb37459af3e5a304332b04b88b993 100644 --- a/core/lib/Drupal/Core/Http/Plugin/SimpletestHttpRequestSubscriber.php +++ b/core/lib/Drupal/Core/Http/Plugin/SimpletestHttpRequestSubscriber.php @@ -34,8 +34,9 @@ public function onBeforeSendRequest(Event $event) { // user-agent is used to ensure that multiple testing sessions running at the // same time won't interfere with each other as they would if the database // prefix were stored statically in a file or database variable. - if ($test_prefix = drupal_valid_test_ua()) { - $event['request']->setHeader('User-Agent', drupal_generate_test_ua($test_prefix)); + $test_info = &$GLOBALS['drupal_test_info']; + if (!empty($test_info['test_run_id'])) { + $event['request']->setHeader('User-Agent', drupal_generate_test_ua($test_info['test_run_id'])); } } } diff --git a/core/lib/Drupal/Core/StreamWrapper/PublicStream.php b/core/lib/Drupal/Core/StreamWrapper/PublicStream.php index 760a1f82e91f4a836d01e695808787d3c89b8fd8..90f17b33049b3ba6a1e12c320bae8057e250b3b3 100644 --- a/core/lib/Drupal/Core/StreamWrapper/PublicStream.php +++ b/core/lib/Drupal/Core/StreamWrapper/PublicStream.php @@ -38,6 +38,13 @@ public function getExternalUrl() { */ public static function basePath() { $base_path = settings()->get('file_public_path', conf_path() . '/files'); + if ($test_prefix = drupal_valid_test_ua()) { + // Append the testing suffix unless already given. + // @see \Drupal\simpletest\WebTestBase::setUp() + if (strpos($base_path, '/simpletest/' . substr($test_prefix, 10)) === FALSE) { + return $base_path . '/simpletest/' . substr($test_prefix, 10); + } + } return $base_path; } diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldImportCreateTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldImportCreateTest.php index 2461165102fa9bd4f48258beba9e96975a406a65..ff1c5846f290f5a6eac69f3da938eeaa5f8d27d9 100644 --- a/core/modules/field/lib/Drupal/field/Tests/FieldImportCreateTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/FieldImportCreateTest.php @@ -95,12 +95,11 @@ function testImportCreate() { // Add the new files to the staging directory. $src_dir = drupal_get_path('module', 'field_test_config') . '/staging'; - $target_dir = $this->configDirectories[CONFIG_STAGING_DIRECTORY]; - $this->assertTrue(file_unmanaged_copy("$src_dir/$field_config_name.yml", "$target_dir/$field_config_name.yml")); - $this->assertTrue(file_unmanaged_copy("$src_dir/$instance_config_name.yml", "$target_dir/$instance_config_name.yml")); - $this->assertTrue(file_unmanaged_copy("$src_dir/$field_config_name_2.yml", "$target_dir/$field_config_name_2.yml")); - $this->assertTrue(file_unmanaged_copy("$src_dir/$instance_config_name_2a.yml", "$target_dir/$instance_config_name_2a.yml")); - $this->assertTrue(file_unmanaged_copy("$src_dir/$instance_config_name_2b.yml", "$target_dir/$instance_config_name_2b.yml")); + $this->assertTrue(file_unmanaged_copy("$src_dir/$field_config_name.yml", "public://config_staging/$field_config_name.yml")); + $this->assertTrue(file_unmanaged_copy("$src_dir/$instance_config_name.yml", "public://config_staging/$instance_config_name.yml")); + $this->assertTrue(file_unmanaged_copy("$src_dir/$field_config_name_2.yml", "public://config_staging/$field_config_name_2.yml")); + $this->assertTrue(file_unmanaged_copy("$src_dir/$instance_config_name_2a.yml", "public://config_staging/$instance_config_name_2a.yml")); + $this->assertTrue(file_unmanaged_copy("$src_dir/$instance_config_name_2b.yml", "public://config_staging/$instance_config_name_2b.yml")); // Import the content of the staging directory. $this->configImporter()->import(); diff --git a/core/modules/file/tests/file_test/lib/Drupal/file_test/DummyReadOnlyStreamWrapper.php b/core/modules/file/tests/file_test/lib/Drupal/file_test/DummyReadOnlyStreamWrapper.php index d230bc5afa2ab33ed74449936e4d8772fdaeb8bf..b427d3f33a7b27dbb62b5f6aa4ed86af3d6a7efd 100644 --- a/core/modules/file/tests/file_test/lib/Drupal/file_test/DummyReadOnlyStreamWrapper.php +++ b/core/modules/file/tests/file_test/lib/Drupal/file_test/DummyReadOnlyStreamWrapper.php @@ -16,7 +16,7 @@ */ class DummyReadOnlyStreamWrapper extends LocalReadOnlyStream { function getDirectoryPath() { - return conf_path() . '/files'; + return 'sites/default/files'; } /** diff --git a/core/modules/file/tests/file_test/lib/Drupal/file_test/DummyStreamWrapper.php b/core/modules/file/tests/file_test/lib/Drupal/file_test/DummyStreamWrapper.php index cbea40f039a53b317e6cd137608ffdfa87c3899e..4836f0955e04005e67e56728bdc1507b6fd60cdf 100644 --- a/core/modules/file/tests/file_test/lib/Drupal/file_test/DummyStreamWrapper.php +++ b/core/modules/file/tests/file_test/lib/Drupal/file_test/DummyStreamWrapper.php @@ -16,7 +16,7 @@ */ class DummyStreamWrapper extends LocalStream { function getDirectoryPath() { - return conf_path() . '/files'; + return 'sites/default/files'; } /** diff --git a/core/modules/language/lib/Drupal/language/Tests/LanguageNegotiationInfoTest.php b/core/modules/language/lib/Drupal/language/Tests/LanguageNegotiationInfoTest.php index 70d297c08f8af2b44a6f82d8774a1e0498da9dcf..58cbd722adabf2a9b90162ad939792658616fb96 100644 --- a/core/modules/language/lib/Drupal/language/Tests/LanguageNegotiationInfoTest.php +++ b/core/modules/language/lib/Drupal/language/Tests/LanguageNegotiationInfoTest.php @@ -23,6 +23,13 @@ class LanguageNegotiationInfoTest extends WebTestBase { */ public static $modules = array('language'); + /** + * The language manager. + * + * @var \Drupal\language\ConfigurableLanguageManagerInterface + */ + protected $languageManager; + /** * {@inheritdoc} */ @@ -39,60 +46,31 @@ public static function getInfo() { */ function setUp() { parent::setUp(); + $this->languageManager = $this->container->get('language_manager'); $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages', 'view the administration theme')); $this->drupalLogin($admin_user); $this->drupalPostForm('admin/config/regional/language/add', array('predefined_langcode' => 'it'), t('Add language')); } - /** - * Returns the configurable language manager. - * - * @return \Drupal\language\ConfigurableLanguageManager - */ - protected function languageManager() { - return $this->container->get('language_manager'); - } - - /** - * Sets state flags for language_test module. - * - * Ensures to correctly update data both in the child site and the test runner - * environment. - * - * @param array $values - * The key/value pairs to set in state. - */ - protected function stateSet(array $values) { - // Set the new state values. - $this->container->get('state')->setMultiple($values); - // Refresh in-memory static state/config caches and static variables. - $this->refreshVariables(); - // Refresh/rewrite language negotiation configuration, in order to pick up - // the manipulations performed by language_test module's info alter hooks. - $this->container->get('language_negotiator')->purgeConfiguration(); - } - /** * Tests alterations to language types/negotiation info. */ function testInfoAlterations() { - $this->stateSet(array( - // Enable language_test type info. - 'language_test.language_types' => TRUE, - // Enable language_test negotiation info (not altered yet). - 'language_test.language_negotiation_info' => TRUE, - // Alter Language::TYPE_CONTENT to be configurable. - 'language_test.content_language_type' => TRUE, - )); - $this->container->get('module_handler')->install(array('language_test')); - $this->rebuildContainer(); + // Enable language type/negotiation info alterations. + \Drupal::state()->set('language_test.language_types', TRUE); + \Drupal::state()->set('language_test.language_negotiation_info', TRUE); + $this->languageNegotiationUpdate(); // Check that fixed language types are properly configured without the need // of saving the language negotiation settings. $this->checkFixedLanguageTypes(); + // Make the content language type configurable by updating the language + // negotiation settings with the proper flag enabled. + \Drupal::state()->set('language_test.content_language_type', TRUE); + $this->languageNegotiationUpdate(); $type = Language::TYPE_CONTENT; - $language_types = $this->languageManager()->getLanguageTypes(); + $language_types = $this->languageManager->getLanguageTypes(); $this->assertTrue(in_array($type, $language_types), 'Content language type is configurable.'); // Enable some core and custom language negotiation methods. The test @@ -109,34 +87,30 @@ function testInfoAlterations() { ); $this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings')); - // Alter language negotiation info to remove interface language negotiation - // method. - $this->stateSet(array( - 'language_test.language_negotiation_info_alter' => TRUE, - )); - - $negotiation = $this->container->get('config.factory')->get('language.types')->get('negotiation.' . $type . '.enabled'); + // Remove the interface language negotiation method by updating the language + // negotiation settings with the proper flag enabled. + \Drupal::state()->set('language_test.language_negotiation_info_alter', TRUE); + $this->languageNegotiationUpdate(); + $negotiation = \Drupal::config('language.types')->get('negotiation.' . $type . '.enabled') ?: array(); $this->assertFalse(isset($negotiation[$interface_method_id]), 'Interface language negotiation method removed from the stored settings.'); - - $this->drupalGet('admin/config/regional/language/detection'); - $this->assertNoFieldByName($form_field, NULL, 'Interface language negotiation method unavailable.'); + $this->assertNoFieldByXPath("//input[@name=\"$form_field\"]", NULL, 'Interface language negotiation method unavailable.'); // Check that type-specific language negotiation methods can be assigned // only to the corresponding language types. - foreach ($this->languageManager()->getLanguageTypes() as $type) { + foreach ($this->languageManager->getLanguageTypes() as $type) { $form_field = $type . '[enabled][test_language_negotiation_method_ts]'; if ($type == $test_type) { - $this->assertFieldByName($form_field, NULL, format_string('Type-specific test language negotiation method available for %type.', array('%type' => $type))); + $this->assertFieldByXPath("//input[@name=\"$form_field\"]", NULL, format_string('Type-specific test language negotiation method available for %type.', array('%type' => $type))); } else { - $this->assertNoFieldByName($form_field, NULL, format_string('Type-specific test language negotiation method unavailable for %type.', array('%type' => $type))); + $this->assertNoFieldByXPath("//input[@name=\"$form_field\"]", NULL, format_string('Type-specific test language negotiation method unavailable for %type.', array('%type' => $type))); } } // Check language negotiation results. $this->drupalGet(''); - $last = $this->container->get('state')->get('language_test.language_negotiation_last'); - foreach ($this->languageManager()->getDefinedLanguageTypes() as $type) { + $last = \Drupal::state()->get('language_test.language_negotiation_last'); + foreach ($this->languageManager->getDefinedLanguageTypes() as $type) { $langcode = $last[$type]; $value = $type == Language::TYPE_CONTENT || strpos($type, 'test') !== FALSE ? 'it' : 'en'; $this->assertEqual($langcode, $value, format_string('The negotiated language for %type is %language', array('%type' => $type, '%language' => $value))); @@ -144,11 +118,10 @@ function testInfoAlterations() { // Uninstall language_test and check that everything is set back to the // original status. - $this->container->get('module_handler')->uninstall(array('language_test')); - $this->rebuildContainer(); + $this->languageNegotiationUpdate('uninstall'); // Check that only the core language types are available. - foreach ($this->languageManager()->getDefinedLanguageTypes() as $type) { + foreach ($this->languageManager->getDefinedLanguageTypes() as $type) { $this->assertTrue(strpos($type, 'test') === FALSE, format_string('The %type language is still available', array('%type' => $type))); } @@ -158,7 +131,7 @@ function testInfoAlterations() { // Check that unavailable language negotiation methods are not present in // the negotiation settings. - $negotiation = $this->container->get('config.factory')->get('language.types')->get('negotiation.' . $type . '.enabled'); + $negotiation = \Drupal::config('language.types')->get('negotiation.' . $type . '.enabled') ?: array(); $this->assertFalse(isset($negotiation[$test_method_id]), 'The disabled test language negotiation method is not part of the content language negotiation settings.'); // Check that configuration page presents the correct options and settings. @@ -166,14 +139,41 @@ function testInfoAlterations() { $this->assertNoRaw(t('This is a test language negotiation method'), 'No test language negotiation method available.'); } + /** + * Update language types/negotiation information. + * + * Manually invoke language_modules_installed()/language_modules_uninstalled() + * since they would not be invoked after installing/uninstalling language_test + * the first time. + */ + protected function languageNegotiationUpdate($op = 'install') { + static $last_op = NULL; + $modules = array('language_test'); + + // Install/uninstall language_test only if we did not already before. + if ($last_op != $op) { + call_user_func(array($this->container->get('module_handler'), $op), $modules); + $last_op = $op; + } + else { + $function = "language_modules_{$op}ed"; + if (function_exists($function)) { + $function($modules); + } + } + + $this->languageManager->reset(); + $this->drupalGet('admin/config/regional/language/detection'); + } + /** * Check that language negotiation for fixed types matches the stored one. */ protected function checkFixedLanguageTypes() { - $configurable = $this->languageManager()->getLanguageTypes(); - foreach ($this->languageManager()->getDefinedLanguageTypesInfo() as $type => $info) { + $configurable = $this->languageManager->getLanguageTypes(); + foreach ($this->languageManager->getDefinedLanguageTypesInfo() as $type => $info) { if (!in_array($type, $configurable) && isset($info['fixed'])) { - $negotiation = $this->container->get('config.factory')->get('language.types')->get('negotiation.' . $type . '.enabled'); + $negotiation = \Drupal::config('language.types')->get('negotiation.' . $type . '.enabled') ?: array(); $equal = count($info['fixed']) == count($negotiation); while ($equal && list($id) = each($negotiation)) { list(, $info_id) = each($info['fixed']); diff --git a/core/modules/node/lib/Drupal/node/Tests/Config/NodeImportCreateTest.php b/core/modules/node/lib/Drupal/node/Tests/Config/NodeImportCreateTest.php index 1d59ca13d599aff1c3db2aa3ea2309cdbfe1f877..c6d4b7a0afdb6914e067f8249d8b25935a35c3e3 100644 --- a/core/modules/node/lib/Drupal/node/Tests/Config/NodeImportCreateTest.php +++ b/core/modules/node/lib/Drupal/node/Tests/Config/NodeImportCreateTest.php @@ -71,8 +71,7 @@ public function testImportCreate() { $this->copyConfig($active, $staging); // Manually add new node type. $src_dir = drupal_get_path('module', 'node_test_config') . '/staging'; - $target_dir = $this->configDirectories[CONFIG_STAGING_DIRECTORY]; - $this->assertTrue(file_unmanaged_copy("$src_dir/$node_type_config_name.yml", "$target_dir/$node_type_config_name.yml")); + $this->assertTrue(file_unmanaged_copy("$src_dir/$node_type_config_name.yml", "public://config_staging/$node_type_config_name.yml")); // Import the content of the staging directory. $this->configImporter()->import(); diff --git a/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php index 6ee194ce7f858896ce78ef6797b55a284df91953..92b161fe44a15582f2d9c62d157dbdc33661f25f 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php @@ -57,13 +57,6 @@ abstract class DrupalUnitTestBase extends UnitTestBase { private $themeFiles; private $themeData; - /** - * The configuration directories for this test run. - * - * @var array - */ - protected $configDirectories = array(); - /** * A KeyValueMemoryFactory instance to use when building the container. * @@ -100,27 +93,6 @@ protected function beforePrepareEnvironment() { } } - /** - * Create and set new configuration directories. - * - * @see config_get_config_directory() - */ - protected function prepareConfigDirectories() { - $this->configDirectories = array(); - include_once DRUPAL_ROOT . '/core/includes/install.inc'; - foreach (array(CONFIG_ACTIVE_DIRECTORY, CONFIG_STAGING_DIRECTORY) as $type) { - // Assign the relative path to the global variable. - $path = $this->siteDirectory . '/config_' . $type; - $GLOBALS['config_directories'][$type] = $path; - // Ensure the directory can be created and is writeable. - if (!install_ensure_config_directory($type)) { - throw new \RuntimeException("Failed to create '$type' config directory $path"); - } - // Provide the already resolved path for tests. - $this->configDirectories[$type] = $path; - } - } - /** * Sets up Drupal unit test environment. */ @@ -129,9 +101,6 @@ protected function setUp() { parent::setUp(); - // Create and set new configuration directories. - $this->prepareConfigDirectories(); - // Build a minimal, partially mocked environment for unit tests. $this->containerBuild(\Drupal::getContainer()); // Make sure it survives kernel rebuilds. @@ -188,9 +157,7 @@ protected function setUp() { } protected function tearDown() { - if ($this->kernel instanceof DrupalKernel) { - $this->kernel->shutdown(); - } + $this->kernel->shutdown(); // Before tearing down the test environment, ensure that no stream wrapper // of this test leaks into the parent environment. Unlike all other global // state variables in Drupal, stream wrappers are a global state construct diff --git a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php index 47ba5111a94ef56e6dea0c783e044ac505640ccb..b6df34700810310e73e26c74c1d2c94ba90283c1 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php @@ -36,13 +36,6 @@ abstract class TestBase { */ protected $testId; - /** - * The site directory of this test run. - * - * @var string - */ - protected $siteDirectory = NULL; - /** * The database prefix of this test run. * @@ -158,13 +151,6 @@ abstract class TestBase { */ public $dieOnFail = FALSE; - /** - * The DrupalKernel instance used in the test. - * - * @var \Drupal\Core\DrupalKernel - */ - protected $kernel; - /** * The dependency injection container used in the test. * @@ -622,20 +608,7 @@ protected function assertIdenticalObject($object1, $object2, $message = '', $gro return $this->assertTrue($identical, $message, $group); } - /** - * Asserts that no errors have been logged to the PHP error.log thus far. - * - * @return bool - * TRUE if the assertion succeeded, FALSE otherwise. - * - * @see TestBase::prepareEnvironment() - * @see _drupal_bootstrap_configuration() - */ - protected function assertNoErrorsLogged() { - // Since PHP only creates the error.log file when an actual error is - // triggered, it is sufficient to check whether the file exists. - return $this->assertFalse(file_exists(DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log'), 'PHP error.log is empty.'); - } + /** * Fire an assertion that is always positive. @@ -884,9 +857,7 @@ public function run(array $methods = array()) { * @see drupal_valid_test_ua() */ private function prepareDatabasePrefix() { - $suffix = mt_rand(1000, 1000000); - $this->siteDirectory = 'sites/simpletest/' . $suffix; - $this->databasePrefix = 'simpletest' . $suffix; + $this->databasePrefix = 'simpletest' . mt_rand(1000, 1000000); // As soon as the database prefix is set, the test might start to execute. // All assertions as well as the SimpleTest batch operations are associated @@ -914,9 +885,19 @@ private function changeDatabasePrefix() { $connection_info = Database::getConnectionInfo('default'); Database::renameConnection('default', 'simpletest_original_default'); foreach ($connection_info as $target => $value) { - $connection_info[$target]['prefix'] = $value['prefix']['default'] . $this->databasePrefix; + $connection_info[$target]['prefix'] = array( + 'default' => $value['prefix']['default'] . $this->databasePrefix, + ); } Database::addConnectionInfo('default', 'default', $connection_info['default']); + + // Additionally override global $databases, since the installer does not use + // the Database connection info. + // @see install_verify_database_settings() + // @see install_database_errors() + // @todo Fix installer to use Database connection info. + global $databases; + $databases['default']['default'] = $connection_info['default']; } /** @@ -957,9 +938,10 @@ private function prepareEnvironment() { $language_interface = language(Language::TYPE_INTERFACE); // When running the test runner within a test, back up the original database - // prefix. - if (DRUPAL_TEST_IN_CHILD_SITE) { + // prefix and re-set the new/nested prefix in drupal_valid_test_ua(). + if (drupal_valid_test_ua()) { $this->originalPrefix = drupal_valid_test_ua(); + drupal_valid_test_ua($this->databasePrefix); } // Backup current in-memory configuration. @@ -999,16 +981,22 @@ private function prepareEnvironment() { // 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->public_files_directory = $this->siteDirectory . '/files'; - $this->private_files_directory = $this->siteDirectory . '/private'; - $this->temp_files_directory = $this->siteDirectory . '/temp'; - $this->translation_files_directory = $this->siteDirectory . '/translations'; - + // Use temporary files directory with the same prefix as the database. + $this->public_files_directory = $this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10); + $this->private_files_directory = $this->public_files_directory . '/private'; + $this->temp_files_directory = $this->private_files_directory . '/temp'; + $this->translation_files_directory = $this->public_files_directory . '/translations'; + + // Create the directories + file_prepare_directory($this->public_files_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + file_prepare_directory($this->private_files_directory, FILE_CREATE_DIRECTORY); + file_prepare_directory($this->temp_files_directory, FILE_CREATE_DIRECTORY); + file_prepare_directory($this->translation_files_directory, FILE_CREATE_DIRECTORY); $this->generatedTestFiles = FALSE; + // Create and set new configuration directories. + $this->prepareConfigDirectories(); + // Unregister all custom stream wrappers of the parent site. // Availability of Drupal stream wrappers varies by test base class: // - UnitTestBase operates in a completely empty environment. @@ -1052,31 +1040,53 @@ private function prepareEnvironment() { \Drupal::setContainer($this->container); // Unset globals. - unset($GLOBALS['config_directories']); unset($GLOBALS['theme_key']); unset($GLOBALS['theme']); // Log fatal errors. ini_set('log_errors', 1); - ini_set('error_log', DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log'); + ini_set('error_log', $this->public_files_directory . '/error.log'); + + // Set the test information for use in other parts of Drupal. + $test_info = &$GLOBALS['drupal_test_info']; + $test_info['test_run_id'] = $this->databasePrefix; + $test_info['in_child_site'] = FALSE; // Change the database prefix. - // All static variables need to be reset before the database prefix is - // changed, since \Drupal\Core\Utility\CacheArray implementations attempt to - // write back to persistent caches when they are destructed. $this->changeDatabasePrefix(); // Remove all configuration overrides. $GLOBALS['config'] = array(); - // After preparing the environment and changing the database prefix, we are - // in a valid test environment. - drupal_valid_test_ua($this->databasePrefix); - conf_path(FALSE, TRUE); - drupal_set_time_limit($this->timeLimit); } + /** + * Create and set new configuration directories. + * + * The child site uses drupal_valid_test_ua() to adjust the config directory + * paths to a test-prefix-specific directory within the public files + * directory. + * + * @see config_get_config_directory() + */ + protected function prepareConfigDirectories() { + $GLOBALS['config_directories'] = array(); + $this->configDirectories = array(); + include_once DRUPAL_ROOT . '/core/includes/install.inc'; + foreach (array(CONFIG_ACTIVE_DIRECTORY, CONFIG_STAGING_DIRECTORY) as $type) { + // Assign the relative path to the global variable. + $path = conf_path() . '/files/simpletest/' . substr($this->databasePrefix, 10) . '/config_' . $type; + $GLOBALS['config_directories'][$type] = $path; + // Ensure the directory can be created and is writeable. + if (!install_ensure_config_directory($type)) { + return FALSE; + } + // Provide the already resolved path for tests. + $this->configDirectories[$type] = $path; + } + } + /** * Rebuild \Drupal::getContainer(). * @@ -1093,11 +1103,11 @@ private function prepareEnvironment() { * tests can invoke this workaround when requiring services from newly * enabled modules to be immediately available in the same request. */ - protected function rebuildContainer($environment = 'testing') { + protected function rebuildContainer() { // Preserve the request object after the container rebuild. $request = \Drupal::request(); - $this->kernel = new DrupalKernel($environment, drupal_classloader(), FALSE); + $this->kernel = new DrupalKernel('testing', drupal_classloader(), FALSE); $this->kernel->boot(); // DrupalKernel replaces the container in \Drupal::getContainer() with a // different object, so we need to replace the instance on this test class. @@ -1159,12 +1169,8 @@ private function restoreEnvironment() { } } - // In case a fatal error occurred that was not in the test process read the - // log to pick up any fatal errors. - simpletest_log_read($this->testId, $this->databasePrefix, get_class($this)); - - // Delete test site directory. - file_unmanaged_delete_recursive($this->siteDirectory, array($this, 'filePreDeleteCallback')); + // Delete temporary files directory. + file_unmanaged_delete_recursive($this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10), array($this, 'filePreDeleteCallback')); // Restore original database connection. Database::removeConnection('default'); @@ -1193,13 +1199,19 @@ private function restoreEnvironment() { \Drupal::setContainer($this->originalContainer); $GLOBALS['config_directories'] = $this->originalConfigDirectories; + // Re-initialize original stream wrappers of the parent site. + // This must happen after static variables have been reset and the original + // container and $config_directories are restored, as simpletest_log_read() + // uses the public stream wrapper to locate the error.log. + file_get_stream_wrappers(); + + // In case a fatal error occurred that was not in the test process read the + // log to pick up any fatal errors. + simpletest_log_read($this->testId, $this->databasePrefix, get_class($this), TRUE); + if (isset($this->originalPrefix)) { drupal_valid_test_ua($this->originalPrefix); } - else { - drupal_valid_test_ua(FALSE); - } - conf_path(TRUE, TRUE); // Restore original shutdown callbacks. $callbacks = &drupal_register_shutdown_function(); diff --git a/core/modules/simpletest/lib/Drupal/simpletest/Tests/BrokenSetUpTest.php b/core/modules/simpletest/lib/Drupal/simpletest/Tests/BrokenSetUpTest.php index 8981519172308d725901a8b1362cb6fc84e8c5a1..42b786ac3fa584e908fbc4ccc188cd24daaa7320 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/Tests/BrokenSetUpTest.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/Tests/BrokenSetUpTest.php @@ -28,13 +28,6 @@ class BrokenSetUpTest extends WebTestBase { */ public static $modules = array('simpletest'); - /** - * The path to the shared trigger file. - * - * @var string - */ - protected $sharedTriggerFile; - public static function getInfo() { return array( 'name' => 'Broken SimpleTest method', @@ -45,20 +38,15 @@ public static function getInfo() { function setUp() { // If the test is being run from the main site, set up normally. - if (!$this->isInChildSite()) { + if (!drupal_valid_test_ua()) { parent::setUp(); - - $this->sharedTriggerFile = $this->public_files_directory . '/trigger'; - // Create and log in user. $admin_user = $this->drupalCreateUser(array('administer unit tests')); $this->drupalLogin($admin_user); } // If the test is being run from within simpletest, set up the broken test. else { - $this->sharedTriggerFile = $this->originalFileDirectory . '/trigger'; - - if (file_get_contents($this->sharedTriggerFile) === 'setup') { + if (file_get_contents($this->originalFileDirectory . '/simpletest/trigger') === 'setup') { throw new \Exception('Broken setup'); } $this->pass('The setUp() method has run.'); @@ -67,13 +55,13 @@ function setUp() { function tearDown() { // If the test is being run from the main site, tear down normally. - if (!$this->isInChildSite()) { - unlink($this->sharedTriggerFile); + if (!drupal_valid_test_ua()) { + unlink($this->originalFileDirectory . '/simpletest/trigger'); parent::tearDown(); } // If the test is being run from within simpletest, output a message. else { - if (file_get_contents($this->sharedTriggerFile) === 'teardown') { + if (file_get_contents($this->originalFileDirectory . '/simpletest/trigger') === 'teardown') { throw new \Exception('Broken teardown'); } $this->pass('The tearDown() method has run.'); @@ -86,9 +74,9 @@ function tearDown() { function testMethod() { // If the test is being run from the main site, run it again from the web // interface within the simpletest child site. - if (!$this->isInChildSite()) { + if (!drupal_valid_test_ua()) { // Verify that a broken setUp() method is caught. - file_put_contents($this->sharedTriggerFile, 'setup'); + file_put_contents($this->originalFileDirectory . '/simpletest/trigger', 'setup'); $edit['Drupal\simpletest\Tests\BrokenSetUpTest'] = TRUE; $this->drupalPostForm('admin/config/development/testing', $edit, t('Run tests')); $this->assertRaw('Broken setup'); @@ -99,7 +87,7 @@ function testMethod() { $this->assertNoRaw('The tearDown() method has run.'); // Verify that a broken tearDown() method is caught. - file_put_contents($this->sharedTriggerFile, 'teardown'); + file_put_contents($this->originalFileDirectory . '/simpletest/trigger', 'teardown'); $edit['Drupal\simpletest\Tests\BrokenSetUpTest'] = TRUE; $this->drupalPostForm('admin/config/development/testing', $edit, t('Run tests')); $this->assertNoRaw('Broken setup'); @@ -110,7 +98,7 @@ function testMethod() { $this->assertNoRaw('The tearDown() method has run.'); // Verify that a broken test method is caught. - file_put_contents($this->sharedTriggerFile, 'test'); + file_put_contents($this->originalFileDirectory . '/simpletest/trigger', 'test'); $edit['Drupal\simpletest\Tests\BrokenSetUpTest'] = TRUE; $this->drupalPostForm('admin/config/development/testing', $edit, t('Run tests')); $this->assertNoRaw('Broken setup'); @@ -122,7 +110,7 @@ function testMethod() { } // If the test is being run from within simpletest, output a message. else { - if (file_get_contents($this->sharedTriggerFile) === 'test') { + if (file_get_contents($this->originalFileDirectory . '/simpletest/trigger') === 'test') { throw new \Exception('Broken test'); } $this->pass('The test method has run.'); diff --git a/core/modules/simpletest/lib/Drupal/simpletest/Tests/MissingCheckedRequirementsTest.php b/core/modules/simpletest/lib/Drupal/simpletest/Tests/MissingCheckedRequirementsTest.php index 24b43483954d7171610773ae049cccddb8d4d7ce..61d6401e53654f8ac128401c69197eb377a375d3 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/Tests/MissingCheckedRequirementsTest.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/Tests/MissingCheckedRequirementsTest.php @@ -39,7 +39,7 @@ function setUp() { * Overrides checkRequirements(). */ protected function checkRequirements() { - if ($this->isInChildSite()) { + if (drupal_valid_test_ua()) { return array( 'Test is not allowed to run.' ); @@ -53,7 +53,7 @@ protected function checkRequirements() { protected function testCheckRequirements() { // If this is the main request, run the web test script and then assert // that the child tests did not run. - if (!$this->isInChildSite()) { + if (!drupal_valid_test_ua()) { // Run this test from web interface. $edit['Drupal\simpletest\Tests\MissingCheckedRequirementsTest'] = TRUE; $this->drupalPostForm('admin/config/development/testing', $edit, t('Run tests')); diff --git a/core/modules/simpletest/lib/Drupal/simpletest/Tests/SimpleTestTest.php b/core/modules/simpletest/lib/Drupal/simpletest/Tests/SimpleTestTest.php index 627ae0baf601de6983796cedeb9df0fbbaa512aa..2a19a5c13c4356c579750835ceba9a53937fcda4 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/Tests/SimpleTestTest.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/Tests/SimpleTestTest.php @@ -39,7 +39,7 @@ public static function getInfo() { } function setUp() { - if (!$this->isInChildSite()) { + if (!$this->inCURL()) { parent::setUp(); // Create and log in an admin user. $this->drupalLogin($this->drupalCreateUser(array('administer unit tests'))); @@ -54,7 +54,7 @@ function setUp() { * Test the internal browsers functionality. */ function testInternalBrowser() { - if (!$this->isInChildSite()) { + if (!$this->inCURL()) { // Retrieve the test page and check its title and headers. $this->drupalGet('test-page'); $this->assertTrue($this->drupalGetHeader('Date'), 'An HTTP header was received.'); @@ -91,12 +91,10 @@ function testInternalBrowser() { $headers = $this->drupalGetHeaders(TRUE); $this->assertEqual(count($headers), 2, 'Simpletest stopped following redirects after the first one.'); - // Remove the Simpletest private key file so we can test the protection + // Remove the Simpletest settings.php so we can test the protection // against requests that forge a valid testing user agent to gain access // to the installer. - // @see drupal_valid_test_ua() - // Not using File API; a potential error must trigger a PHP warning. - unlink($this->siteDirectory . '/.htkey'); + drupal_unlink($this->public_files_directory . '/settings.php'); global $base_url; $this->drupalGet(url($base_url . '/core/install.php', array('external' => TRUE, 'absolute' => TRUE))); $this->assertResponse(403, 'Cannot access install.php.'); @@ -107,7 +105,7 @@ function testInternalBrowser() { * Test validation of the User-Agent header we use to perform test requests. */ function testUserAgentValidation() { - if (!$this->isInChildSite()) { + if (!$this->inCURL()) { global $base_url; $system_path = $base_url . '/' . drupal_get_path('module', 'system'); $HTTP_path = $system_path .'/tests/http.php?q=node'; @@ -149,7 +147,7 @@ function testWebTestRunner() { $this->valid_permission = 'access content'; $this->invalid_permission = 'invalid permission'; - if ($this->isInChildSite()) { + if ($this->inCURL()) { // Only run following code if this test is running itself through a CURL // request. $this->stubTest(); @@ -337,4 +335,10 @@ function asText(\SimpleXMLElement $element) { return trim(html_entity_decode(strip_tags($element->asXML()))); } + /** + * Check if the test is being run from inside a CURL request. + */ + function inCURL() { + return (bool) drupal_valid_test_ua(); + } } diff --git a/core/modules/simpletest/lib/Drupal/simpletest/UnitTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/UnitTestBase.php index 643f6529d8094af430f9be06764f05c526b248e2..86a725435cc5fef938f84b3a73af11963f7fcab3 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/UnitTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/UnitTestBase.php @@ -19,6 +19,11 @@ */ abstract class UnitTestBase extends TestBase { + /** + * @var array + */ + protected $configDirectories; + /** * Constructor for UnitTestBase. */ @@ -36,7 +41,6 @@ function __construct($test_id = NULL) { * setUp() method. */ protected function setUp() { - file_prepare_directory($this->public_files_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); $this->settingsSet('file_public_path', $this->public_files_directory); } } diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php index f5b8cb9b87f01d597d6c83847f9e278ec72c98af..3917ffae8b22bbc1ab33d7284db0b64e7f6d26c1 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php @@ -767,78 +767,46 @@ protected function setUp() { $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. - copy(DRUPAL_ROOT . '/sites/default/default.settings.php', DRUPAL_ROOT . '/' . $this->siteDirectory . '/settings.php'); - - // 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->public_files_directory, - 'required' => TRUE, - ); - // Add the parent profile's search path to the child site's search paths. - // @see drupal_system_listing() - $settings['conf']['simpletest.settings']['parent_profile'] = (object) array( - 'value' => $this->originalProfile, - 'required' => TRUE, - ); - $this->writeSettings($settings); - - // Since Drupal is bootstrapped already, install_begin_request() will not - // bootstrap into DRUPAL_BOOTSTRAP_CONFIGURATION (again). Hence, we have to - // reload the newly written custom settings.php manually. - drupal_settings_initialize(); + $this->settingsSet('file_public_path', $this->public_files_directory); + $GLOBALS['config']['system.file']['path']['private'] = $this->private_files_directory; + $GLOBALS['config']['system.file']['path']['temporary'] = $this->temp_files_directory; + $GLOBALS['config']['locale.settings']['translation']['path'] = $this->translation_files_directory; // Execute the non-interactive installer. require_once DRUPAL_ROOT . '/core/includes/install.core.inc'; + $this->settingsSet('cache', array('default' => 'cache.backend.memory')); + $parameters = $this->installParameters(); install_drupal($parameters); - // Import new settings.php written by the installer. - drupal_settings_initialize(); - foreach ($GLOBALS['config_directories'] as $type => $path) { - $this->configDirectories[$type] = $path; - } - - // 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(DRUPAL_ROOT . '/' . $this->siteDirectory, 0777); + // Set the install_profile so that web requests to the requests to the child + // site have the correct profile. + $settings = array( + 'settings' => array( + 'install_profile' => (object) array( + 'value' => $this->profile, + 'required' => TRUE, + ), + ), + ); + $this->writeSettings($settings); + // Override install profile in Settings to so the correct profile is used by + // tests. + $this->settingsSet('install_profile', $this->profile); + $this->settingsSet('cache', array()); $this->rebuildContainer(); - // 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->private_files_directory, FILE_CREATE_DIRECTORY); - file_prepare_directory($this->temp_files_directory, FILE_CREATE_DIRECTORY); - \Drupal::config('system.file') - ->set('path.private', $this->private_files_directory) - ->set('path.temporary', $this->temp_files_directory) - ->save(); - - // Manually configure the test mail collector implementation to prevent - // tests from sending out e-mails 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. - \Drupal::config('system.mail') - ->set('interface.default', 'Drupal\Core\Mail\TestMailCollector') - ->save(); - // Restore the original Simpletest batch. $batch = &batch_get(); $batch = $this->originalBatch; + // Set path variables. + + // Set 'parent_profile' of simpletest to add the parent profile's + // search path to the child site's search paths. + // @see drupal_system_listing() + \Drupal::config('simpletest.settings')->set('parent_profile', $this->originalProfile)->save(); + // Collect modules to install. $class = get_class($this); $modules = array(); @@ -855,18 +823,22 @@ protected function setUp() { $this->rebuildContainer(); } - // Like DRUPAL_BOOTSTRAP_CONFIGURATION above, any further bootstrap phases - // are not re-executed by the installer, as Drupal is bootstrapped already. - // 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. - // Affects e.g. file_get_stream_wrappers(). - // @see _drupal_bootstrap_code() - // @see _drupal_bootstrap_full() - // @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. + // Reset/rebuild all data structures after enabling the modules. $this->resetAll(); + // Now make sure that the file path configurations are saved. This is done + // after we install the modules to override default values. + \Drupal::config('system.file') + ->set('path.private', $this->private_files_directory) + ->set('path.temporary', $this->temp_files_directory) + ->save(); + \Drupal::config('locale.settings') + ->set('translation.path', $this->translation_files_directory) + ->save(); + + // Use the test mail class instead of the default mail handler class. + \Drupal::config('system.mail')->set('interface.default', 'Drupal\Core\Mail\TestMailCollector')->save(); + // Temporary fix so that when running from run-tests.sh we don't get an // empty current path which would indicate we're on the home page. $path = current_path(); @@ -923,29 +895,40 @@ protected function installParameters() { } /** - * Rewrites the settings.php file of the test site. + * Writes a test-specific settings.php file for the child site. * - * @param array $settings - * An array of settings to write out, in the format expected by - * drupal_rewrite_settings(). + * The child site loads this after the parent site's settings.php, so settings + * here override those. * + * @param $settings An array of settings to write out, in the format expected + * by drupal_rewrite_settings(). + * + * @see _drupal_load_test_overrides() * @see drupal_rewrite_settings() */ - protected function writeSettings(array $settings) { + protected function writeSettings($settings) { + // drupal_rewrite_settings() sets the in-memory global variables in addition + // to writing the file. We'll want to restore the original globals. + foreach (array_keys($settings) as $variable_name) { + $original_globals[$variable_name] = isset($GLOBALS[$variable_name]) ? $GLOBALS[$variable_name] : NULL; + } + include_once DRUPAL_ROOT . '/core/includes/install.inc'; - $filename = $this->siteDirectory . '/settings.php'; - // system_requirements() removes write permissions from settings.php - // whenever it is invoked. - // Not using File API; a potential error must trigger a PHP warning. - chmod($filename, 0666); + $filename = $this->public_files_directory . '/settings.php'; + file_put_contents($filename, "<?php\n"); drupal_rewrite_settings($settings, $filename); + + // Restore the original globals. + foreach ($original_globals as $variable_name => $value) { + $GLOBALS[$variable_name] = $value; + } } /** - * Queues custom translations to be written to settings.php. + * Sets custom translations to the settings object and queues them to writing. * - * Use WebTestBase::writeCustomTranslations() to apply and write the queued - * translations. + * In order for those custom translations to persist (being written in test + * site's settings.php) make sure to also call self::writeCustomTranslations() * * @param string $langcode * The langcode to add translations for. @@ -958,56 +941,32 @@ protected function writeSettings(array $settings) { * 'Long month name' => array('March' => 'marzo'), * ); * @endcode - * Pass an empty array to remove all existing custom translations for the - * given $langcode. */ protected function addCustomTranslations($langcode, array $values) { - // If $values is empty, then the test expects all custom translations to be - // cleared. - if (empty($values)) { - $this->customTranslations[$langcode] = array(); - } - // Otherwise, $values are expected to be merged into previously passed - // values, while retaining keys that are not explicitly set. - else { - foreach ($values as $context => $translations) { - foreach ($translations as $original => $translation) { - $this->customTranslations[$langcode][$context][$original] = $translation; - } + $this->settingsSet('locale_custom_strings_' . $langcode, $values); + foreach ($values as $key => $translations) { + foreach ($translations as $label => $value) { + $this->customTranslations['locale_custom_strings_' . $langcode][$key][$label] = (object) array( + 'value' => $value, + 'required' => TRUE, + ); } } } /** - * Writes custom translations to the test site's settings.php. - * - * Use TestBase::addCustomTranslations() to queue custom translations before - * calling this method. + * Writes custom translations to test site's settings.php. */ protected function writeCustomTranslations() { - $settings = array(); - foreach ($this->customTranslations as $langcode => $values) { - $settings_key = 'locale_custom_strings_' . $langcode; - - // Update in-memory settings directly. - $this->settingsSet($settings_key, $values); - - $settings['settings'][$settings_key] = (object) array( - 'value' => $values, - 'required' => TRUE, - ); - } - // Only rewrite settings if there are any translation changes to write. - if (!empty($settings)) { - $this->writeSettings($settings); - } + $this->writeSettings(array('settings' => $this->customTranslations)); + $this->customTranslations = array(); } /** * Overrides \Drupal\simpletest\TestBase::rebuildContainer(). */ - protected function rebuildContainer($environment = 'prod') { - parent::rebuildContainer($environment); + protected function rebuildContainer() { + parent::rebuildContainer(); // Make sure the url generator has a request object, otherwise calls to // $this->drupalGet() will fail. $this->prepareRequestForGenerator(); @@ -1047,8 +1006,8 @@ protected function refreshVariables() { // Clear the tag cache. drupal_static_reset('Drupal\Core\Cache\CacheBackendInterface::tagCache'); - $this->container->get('config.factory')->reset(); - $this->container->get('state')->resetCache(); + \Drupal::service('config.factory')->reset(); + \Drupal::state()->resetCache(); } /** @@ -1311,21 +1270,6 @@ protected function curlClose() { } } - /** - * Returns whether the test is being executed from within a test site. - * - * Mainly used by recursive tests (i.e. to test the testing framework). - * - * @return bool - * TRUE if this test was instantiated in a request within the test site, - * FALSE otherwise. - * - * @see _drupal_bootstrap_configuration() - */ - protected function isInChildSite() { - return DRUPAL_TEST_IN_CHILD_SITE; - } - /** * Parse content returned from curlExec using DOM and SimpleXML. * @@ -1369,18 +1313,10 @@ protected function parse() { protected function drupalGet($path, array $options = array(), array $headers = array()) { $options['absolute'] = TRUE; - // The URL generator service is not necessarily available yet; e.g., in - // interactive installer tests. - if ($this->container->has('url_generator')) { - $url = $this->container->get('url_generator')->generateFromPath($path, $options); - } - else { - $url = $this->getAbsoluteUrl($path); - } - // We re-using a CURL connection here. If that connection still has certain // options set, it might change the GET into a POST. Make sure we clear out // previous options. + $url = $this->container->get('url_generator')->generateFromPath($path, $options); $out = $this->curlExec(array(CURLOPT_HTTPGET => TRUE, CURLOPT_URL => $url, CURLOPT_NOBODY => FALSE, CURLOPT_HTTPHEADER => $headers)); // Ensure that any changes to variables in the other thread are picked up. $this->refreshVariables(); @@ -1900,28 +1836,6 @@ protected function serializePostValues($post = array()) { return implode('&', $post); } - /** - * Transforms a nested array into a flat array suitable for WebTestBase::drupalPostForm(). - * - * @param array $values - * A multi-dimensional form values array to convert. - * - * @return array - * The flattened $edit array suitable for WebTestBase::drupalPostForm(). - */ - protected function translatePostValues(array $values) { - $edit = array(); - // The easiest and most straightforward way to translate values suitable for - // WebTestBase::drupalPostForm() is to actually build the POST data string - // and convert the resulting key/value pairs back into a flat array. - $query = http_build_query($values); - foreach (explode('&', $query) as $item) { - list($key, $value) = explode('=', $item); - $edit[urldecode($key)] = urldecode($value); - } - return $edit; - } - /** * Runs cron in the Drupal installed by Simpletest. */ diff --git a/core/modules/simpletest/simpletest.api.php b/core/modules/simpletest/simpletest.api.php index dad9a397806d00e7c2df0a1c0691a3081deabd96..e11c651f65f9601a23c0ac7216a216f6b49c8d39 100644 --- a/core/modules/simpletest/simpletest.api.php +++ b/core/modules/simpletest/simpletest.api.php @@ -5,6 +5,18 @@ * Hooks provided by the SimpleTest module. */ +/** + * Global variable that holds information about the tests being run. + * + * An array, with the following keys: + * - 'test_run_id': the ID of the test being run, in the form 'simpletest_%" + * - 'in_child_site': TRUE if the current request is a cURL request from + * the parent site. + * + * @var array + */ +global $drupal_test_info; + /** * @addtogroup hooks * @{ diff --git a/core/modules/simpletest/simpletest.install b/core/modules/simpletest/simpletest.install index 917a938cda8df9509389b0a516e36c6d9a427af4..3421dd0a745241bfb0787b1585f49e8c28db7c79 100644 --- a/core/modules/simpletest/simpletest.install +++ b/core/modules/simpletest/simpletest.install @@ -58,28 +58,6 @@ function simpletest_requirements($phase) { $requirements['php_memory_limit']['description'] = t('The testing framework requires the PHP memory limit to be at least %memory_minimum_limit. The current value is %memory_limit. <a href="@url">Follow these steps to continue</a>.', array('%memory_limit' => $memory_limit, '%memory_minimum_limit' => SIMPLETEST_MINIMUM_PHP_MEMORY_LIMIT, '@url' => 'http://drupal.org/node/207036')); } - $site_directory = 'sites/simpletest'; - if (!drupal_verify_install_file(DRUPAL_ROOT . '/' . $site_directory, FILE_EXIST|FILE_READABLE|FILE_WRITABLE|FILE_EXECUTABLE, 'dir')) { - $requirements['simpletest_site_directory'] = array( - 'title' => t('Simpletest site directory'), - 'value' => is_dir(DRUPAL_ROOT . '/' . $site_directory) ? t('Not writable') : t('Missing'), - 'severity' => REQUIREMENT_ERROR, - 'description' => t('The testing framework requires the !sites-simpletest directory to exist and be writable in order to run tests.', array( - '!sites-simpletest' => '<code>./' . check_plain($site_directory) . '</code>', - )), - ); - } - elseif (!file_save_htaccess(DRUPAL_ROOT . '/' . $site_directory, FALSE)) { - $requirements['simpletest_site_directory'] = array( - 'title' => t('Simpletest site directory'), - 'value' => t('Not protected'), - 'severity' => REQUIREMENT_ERROR, - 'description' => t('The file !file does not exist and could not be created automatically, which poses a security risk. Ensure that the directory is writable.', array( - '!file' => '<code>./' . check_plain($site_directory) . '/.htaccess</code>', - )), - ); - } - return $requirements; } @@ -179,12 +157,8 @@ function simpletest_schema() { * Implements hook_uninstall(). */ function simpletest_uninstall() { - // Do not clean the environment in case the Simpletest module is uninstalled - // in a (recursive) test for itself, since simpletest_clean_environment() - // would also delete the test site of the parent test process. - if (!DRUPAL_TEST_IN_CHILD_SITE) { - simpletest_clean_environment(); - } - // Delete verbose test output and any other testing framework files. + simpletest_clean_database(); + + // Remove generated files. file_unmanaged_delete_recursive('public://simpletest'); } diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module index 82db60ee7dfc7ad591b2e0338dc4e67633a547a8..c6b53bf0c4055c406c8c22750aa5b9f4d1417a0a 100644 --- a/core/modules/simpletest/simpletest.module +++ b/core/modules/simpletest/simpletest.module @@ -383,16 +383,18 @@ function simpletest_last_test_get($test_id) { * * @param $test_id * The test ID to which the log relates. - * @param $database_prefix + * @param $prefix * The database prefix to which the log relates. * @param $test_class * The test class to which the log relates. - * + * @param $during_test + * Indicates that the current file directory path is a temporary file + * file directory used during testing. * @return * Found any entries in log. */ -function simpletest_log_read($test_id, $database_prefix, $test_class) { - $log = DRUPAL_ROOT . '/sites/simpletest/' . substr($database_prefix, 10) . '/error.log'; +function simpletest_log_read($test_id, $prefix, $test_class, $during_test = FALSE) { + $log = 'public://' . ($during_test ? '' : '/simpletest/' . substr($prefix, 10)) . '/error.log'; $found = FALSE; if (file_exists($log)) { foreach (file($log) as $line) { @@ -646,11 +648,11 @@ function simpletest_clean_database() { */ function simpletest_clean_temporary_directories() { $count = 0; - if (is_dir(DRUPAL_ROOT . '/sites/simpletest')) { - $files = scandir(DRUPAL_ROOT . '/sites/simpletest'); + if (is_dir('public://simpletest')) { + $files = scandir('public://simpletest'); foreach ($files as $file) { - if ($file[0] != '.') { - $path = DRUPAL_ROOT . '/sites/simpletest/' . $file; + $path = 'public://simpletest/' . $file; + if (is_dir($path) && (is_numeric($file) || strpos($file, 'config_simpletest') !== FALSE)) { file_unmanaged_delete_recursive($path, array('Drupal\simpletest\TestBase', 'filePreDeleteCallback')); $count++; } diff --git a/core/modules/system/lib/Drupal/system/Tests/Ajax/FormValuesTest.php b/core/modules/system/lib/Drupal/system/Tests/Ajax/FormValuesTest.php index 6fb3ab18deab622749525dce273b5cfe16312d75..d75db82f6709bbf814b345d79077bc9470c64532 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Ajax/FormValuesTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Ajax/FormValuesTest.php @@ -53,8 +53,6 @@ function testSimpleAjaxFormValue() { } // Verify that AJAX elements with invalid callbacks return error code 500. - // Ensure the test error log is empty before these tests. - $this->assertNoErrorsLogged(); foreach (array('null', 'empty', 'nonexistent') as $key) { $element_name = 'select_' . $key . '_callback'; $edit = array( @@ -63,8 +61,5 @@ function testSimpleAjaxFormValue() { $commands = $this->drupalPostAjaxForm('ajax_forms_test_get_form', $edit, $element_name); $this->assertResponse(500); } - // The exceptions are expected. Do not interpret them as a test failure. - // Not using File API; a potential error must trigger a PHP warning. - unlink(DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log'); } } diff --git a/core/modules/system/lib/Drupal/system/Tests/DrupalKernel/DrupalKernelTest.php b/core/modules/system/lib/Drupal/system/Tests/DrupalKernel/DrupalKernelTest.php index 76d0fde611d762b658e81290e69e0b580fe4d0dc..ae61ae5706effc89b41de58858c249c8411f24fe 100644 --- a/core/modules/system/lib/Drupal/system/Tests/DrupalKernel/DrupalKernelTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/DrupalKernel/DrupalKernelTest.php @@ -10,12 +10,12 @@ use Drupal\Core\DrupalKernel; use Drupal\Component\PhpStorage\MTimeProtectedFastFileStorage; use Drupal\Component\PhpStorage\FileReadOnlyStorage; -use Drupal\simpletest\DrupalUnitTestBase; +use Drupal\simpletest\UnitTestBase; /** * Tests compilation of the DIC. */ -class DrupalKernelTest extends DrupalUnitTestBase { +class DrupalKernelTest extends UnitTestBase { public static function getInfo() { return array( @@ -26,13 +26,7 @@ public static function getInfo() { } function setUp() { - // DrupalKernel relies on global $config_directories and requires those - // directories to exist. Therefore, create the directories, but do not - // invoke DrupalUnitTestBase::setUp(), since that would set up further - // environment aspects, which would distort this test, because it tests - // the DrupalKernel (re-)building itself. - $this->prepareConfigDirectories(); - + parent::setUp(); $this->settingsSet('php_storage', array('service_container' => array( 'bin' => 'service_container', 'class' => 'Drupal\Component\PhpStorage\MTimeProtectedFileStorage', diff --git a/core/modules/system/lib/Drupal/system/Tests/Installer/InstallerTranslationTest.php b/core/modules/system/lib/Drupal/system/Tests/Installer/InstallerTranslationTest.php index 2726a2266a4a7c0adbd22d8bb66102f3e367c257..399cbc5255fefdc00a64d85c6438539f4422b063 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Installer/InstallerTranslationTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Installer/InstallerTranslationTest.php @@ -7,6 +7,7 @@ namespace Drupal\system\Tests\Installer; +use Drupal\Component\Utility\NestedArray; use Drupal\system\Tests\InstallerTest; /** @@ -15,11 +16,11 @@ class InstallerTranslationTest extends InstallerTest { /** - * Overrides the language code in which to install Drupal. + * Whether the installer has completed. * - * @var string + * @var bool */ - protected $langcode = 'de'; + protected $isInstalled = FALSE; public static function getInfo() { return array( @@ -29,34 +30,104 @@ public static function getInfo() { ); } - /** - * Overrides InstallerTest::setUpLanguage(). - */ - protected function setUpLanguage() { - parent::setUpLanguage(); - // After selecting a different language than English, all following screens - // should be translated already. - // @todo Instead of actually downloading random translations that cannot be - // asserted, write and supply a German translation file. Until then, take - // over whichever string happens to be there, but ensure that the English - // string no longer appears. - $elements = $this->xpath('//input[@type="submit"]/@value'); - $string = (string) current($elements); - $this->assertNotEqual($string, 'Save and continue'); - $this->translations['Save and continue'] = $string; - } + protected function setUp() { + $this->isInstalled = FALSE; - /** - * Overrides InstallerTest::setUpConfirm(). - */ - protected function setUpConfirm() { - // We don't know the translated link text of "Visit your new site", but - // luckily, there is only one link. - $elements = $this->xpath('//a'); - $string = (string) current($elements); - $this->assertNotEqual($string, 'Visit your new site'); - $this->translations['Visit your new site'] = $string; - parent::setUpConfirm(); + + $settings['conf_path'] = (object) array( + 'value' => $this->public_files_directory, + 'required' => TRUE, + ); + $settings['config_directories'] = (object) array( + 'value' => array(), + 'required' => TRUE, + ); + $settings['config']['system.file'] = (object) array( + 'value' => array( + 'path' => array( + 'private' => $this->private_files_directory, + 'temporary' => $this->temp_files_directory, + ), + ), + 'required' => TRUE, + ); + // Add the translations directory so we can retrieve German translations. + $settings['config']['locale.settings'] = (object) array( + 'value' => array( + 'translation' => array( + 'path' => drupal_get_path('module', 'simpletest') . '/files/translations', + ), + ), + 'required' => TRUE, + ); + $this->writeSettings($settings); + + // Submit the installer with German language. + $edit = array( + 'langcode' => 'de', + ); + $this->drupalPostForm($GLOBALS['base_url'] . '/core/install.php', $edit, 'Save and continue'); + + // On the following page where installation profile is being selected the + // interface should be already translated, so there is no "Set up database" + // text anymore. + $this->assertNoText('Set up database', '"Set up database" string was not found.'); + + // After this assertion all we needed to test is tested, but the test + // expects the installation to succeed. If the test would finish here, an + // exception would occur. That is why the full installation has to be + // finished in the further steps. + + // Get the "Save and continue" submit button translated value from the + // translated interface. + $submit_value = (string) current($this->xpath('//input[@type="submit"]/@value')); + $this->assertNotEqual($submit_value, 'Save and continue'); + + // Submit the Standard profile installation. + $edit = array( + 'profile' => 'standard', + ); + $this->drupalPostForm(NULL, $edit, $submit_value); + + // Submit the next step. + $this->drupalPostForm(NULL, array(), $submit_value); + + // Reload config directories. + include $this->public_files_directory . '/settings.php'; + foreach ($config_directories as $type => $path) { + $GLOBALS['config_directories'][$type] = $path; + } + $this->rebuildContainer(); + + \Drupal::config('system.file') + ->set('path.private', $this->private_files_directory) + ->set('path.temporary', $this->temp_files_directory) + ->save(); + \Drupal::config('locale.settings') + ->set('translation.path', $this->translation_files_directory) + ->save(); + + // Submit site configuration form. + $this->drupalPostForm(NULL, array( + 'site_mail' => 'admin@test.de', + 'account[name]' => 'admin', + 'account[mail]' => 'admin@test.de', + 'account[pass][pass1]' => '123', + 'account[pass][pass2]' => '123', + 'site_default_country' => 'DE', + ), $submit_value); + + // Use the test mail class instead of the default mail handler class. + \Drupal::config('system.mail')->set('interface.default', 'Drupal\Core\Mail\TestMailCollector')->save(); + + // When running from run-tests.sh we don't get an empty current path which + // would indicate we're on the home page. + $path = current_path(); + if (empty($path)) { + _current_path('run-tests'); + } + + $this->isInstalled = TRUE; } } diff --git a/core/modules/system/lib/Drupal/system/Tests/InstallerTest.php b/core/modules/system/lib/Drupal/system/Tests/InstallerTest.php index 34a82ac4d6f97fe5a1bc6363eab36956f151e902..e788a6fc402235472926efa22c5f4847bbce4c5d 100644 --- a/core/modules/system/lib/Drupal/system/Tests/InstallerTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/InstallerTest.php @@ -8,60 +8,13 @@ namespace Drupal\system\Tests; use Drupal\Component\Utility\NestedArray; -use Drupal\Core\Session\UserSession; use Drupal\simpletest\WebTestBase; /** * Allows testing of the interactive installer. - * - * @todo Move majority of code into new Drupal\simpletest\InstallerTestBase. */ class InstallerTest extends WebTestBase { - /** - * Custom settings.php values to write for a test run. - * - * @var array - * An array of settings to write out, in the format expected by - * drupal_rewrite_settings(). - */ - protected $settings = array(); - - /** - * The language code in which to install Drupal. - * - * @var string - */ - protected $langcode = 'en'; - - /** - * The installation profile to install. - * - * @var string - */ - protected $profile = 'minimal'; - - /** - * Additional parameters to use for installer screens. - * - * @see WebTestBase::installParameters() - * - * @var array - */ - protected $parameters = array(); - - /** - * A string translation map used for translated installer screens. - * - * Keys are English strings, values are translated strings. - * - * @var array - */ - protected $translations = array( - 'Save and continue' => 'Save and continue', - 'Visit your new site' => 'Visit your new site', - ); - /** * Whether the installer has completed. * @@ -77,74 +30,57 @@ public static function getInfo() { ); } - /** - * Overrides WebTestBase::setUp(). - */ protected function setUp() { $this->isInstalled = FALSE; - // Define information about the user 1 account. - $this->root_user = new UserSession(array( - 'uid' => 1, - 'name' => 'admin', - 'mail' => 'admin@example.com', - 'pass_raw' => $this->randomName(), - )); - - // If any $settings are defined for this test, copy and prepare an actual - // settings.php, so as to resemble a regular installation. - if (!empty($this->settings)) { - // Not using File API; a potential error must trigger a PHP warning. - copy(DRUPAL_ROOT . '/sites/default/default.settings.php', DRUPAL_ROOT . '/' . $this->siteDirectory . '/settings.php'); - $this->writeSettings($settings); - } - - // Note that WebTestBase::installParameters() returns form input values - // suitable for a programmed drupal_form_submit(). - // @see WebTestBase::translatePostValues() - $this->parameters = $this->installParameters(); - - $this->drupalGet($GLOBALS['base_url'] . '/core/install.php'); - // Select language. - $this->setUpLanguage(); - // Select profile. - $this->setUpProfile(); - - // Configure settings. - $this->setUpSettings(); - - // @todo Allow test classes based on this class to act on further installer - // screens. - - // Configure site. - $this->setUpSite(); - - // Confirm installation. - $this->setUpConfirm(); - - // Import new settings.php written by the installer. - drupal_settings_initialize(); - foreach ($GLOBALS['config_directories'] as $type => $path) { - $this->configDirectories[$type] = $path; + $settings['conf_path'] = (object) array( + 'value' => $this->public_files_directory, + 'required' => TRUE, + ); + $settings['config_directories'] = (object) array( + 'value' => array(), + 'required' => TRUE, + ); + $settings['config']['system.file'] = (object) array( + 'value' => array( + 'path' => array( + 'private' => $this->private_files_directory, + 'temporary' => $this->temp_files_directory, + ), + ), + 'required' => TRUE, + ); + $settings['config']['locale.settings'] = (object) array( + 'value' => array( + 'translation' => array( + 'path' => $this->translation_files_directory, + ), + ), + 'required' => TRUE, + ); + $this->writeSettings($settings); + + $this->drupalGet($GLOBALS['base_url'] . '/core/install.php?langcode=en&profile=minimal'); + $this->drupalPostForm(NULL, array(), 'Save and continue'); + // Reload config directories. + include $this->public_files_directory . '/settings.php'; + foreach ($config_directories as $type => $path) { + $GLOBALS['config_directories'][$type] = $path; } - - // 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. - // WebTestBase::tearDown() will delete the entire test site directory. - // Not using File API; a potential error must trigger a PHP warning. - chmod(DRUPAL_ROOT . '/' . $this->siteDirectory, 0777); - $this->rebuildContainer(); - // Manually configure the test mail collector implementation to prevent - // tests from sending out e-mails and collect them in state instead. - \Drupal::config('system.mail') - ->set('interface.default', 'Drupal\Core\Mail\TestMailCollector') + \Drupal::config('system.file') + ->set('path.private', $this->private_files_directory) + ->set('path.temporary', $this->temp_files_directory) ->save(); + \Drupal::config('locale.settings') + ->set('translation.path', $this->translation_files_directory) + ->save(); + + // Use the test mail class instead of the default mail handler class. + \Drupal::config('system.mail')->set('interface.default', 'Drupal\Core\Mail\TestMailCollector')->save(); // When running from run-tests.sh we don't get an empty current path which // would indicate we're on the home page. @@ -156,49 +92,6 @@ protected function setUp() { $this->isInstalled = TRUE; } - /** - * Installer step: Select language. - */ - protected function setUpLanguage() { - $edit = array( - 'langcode' => $this->langcode, - ); - $this->drupalPostForm(NULL, $edit, $this->translations['Save and continue']); - } - - /** - * Installer step: Select installation profile. - */ - protected function setUpProfile() { - $edit = array( - 'profile' => $this->profile, - ); - $this->drupalPostForm(NULL, $edit, $this->translations['Save and continue']); - } - - /** - * Installer step: Configure settings. - */ - protected function setUpSettings() { - $edit = $this->translatePostValues($this->parameters['forms']['install_settings_form']); - $this->drupalPostForm(NULL, $edit, $this->translations['Save and continue']); - } - - /** - * Installer step: Configure site. - */ - protected function setUpSite() { - $edit = $this->translatePostValues($this->parameters['forms']['install_configure_form']); - $this->drupalPostForm(NULL, $edit, $this->translations['Save and continue']); - } - - /** - * Installer step: Confirm installation. - */ - protected function setUpConfirm() { - $this->clickLink($this->translations['Visit your new site']); - } - /** * {@inheritdoc} * @@ -211,14 +104,35 @@ protected function refreshVariables() { } } + /** + * {@inheritdoc} + * + * This override is necessary because the parent drupalGet() calls t(), which + * is not available early during installation. + */ + protected function drupalGet($path, array $options = array(), array $headers = array()) { + // We are re-using a CURL connection here. If that connection still has + // certain options set, it might change the GET into a POST. Make sure we + // clear out previous options. + $out = $this->curlExec(array(CURLOPT_HTTPGET => TRUE, CURLOPT_URL => $this->getAbsoluteUrl($path), CURLOPT_NOBODY => FALSE, CURLOPT_HTTPHEADER => $headers)); + $this->refreshVariables(); // Ensure that any changes to variables in the other thread are picked up. + + // Replace original page output with new output from redirected page(s). + if ($new = $this->checkForMetaRefresh()) { + $out = $new; + } + $this->verbose('GET request to: ' . $path . + '<hr />Ending URL: ' . $this->getUrl() . + '<hr />' . $out); + return $out; + } + /** * Ensures that the user page is available after every test installation. */ public function testInstaller() { - $this->assertUrl('user/1'); + $this->drupalGet('user'); $this->assertResponse(200); - // Confirm that we are logged-in after installation. - $this->assertText($this->root_user->getUsername()); } } diff --git a/core/modules/system/lib/Drupal/system/Tests/System/ErrorHandlerTest.php b/core/modules/system/lib/Drupal/system/Tests/System/ErrorHandlerTest.php index 2356a849cfcf9eda40bd0a0ca5b92691e8a111b3..7d20be9bc8ca123776b90dd8a6ddcf53bafe7f2a 100644 --- a/core/modules/system/lib/Drupal/system/Tests/System/ErrorHandlerTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/System/ErrorHandlerTest.php @@ -94,9 +94,6 @@ function testErrorHandler() { * Test the exception handler. */ function testExceptionHandler() { - // Ensure the test error log is empty before these tests. - $this->assertNoErrorsLogged(); - $error_exception = array( '%type' => 'Exception', '!message' => 'Drupal is awesome', @@ -124,10 +121,6 @@ function testExceptionHandler() { $this->assertText($error_pdo_exception['!message'], format_string('Found !message in error page.', $error_pdo_exception)); $error_details = format_string('in %function (line ', $error_pdo_exception); $this->assertRaw($error_details, format_string("Found '!message' in error page.", array('!message' => $error_details))); - - // The exceptions are expected. Do not interpret them as a test failure. - // Not using File API; a potential error must trigger a PHP warning. - unlink(DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log'); } /** diff --git a/core/modules/system/lib/Drupal/system/Tests/System/ShutdownFunctionsTest.php b/core/modules/system/lib/Drupal/system/Tests/System/ShutdownFunctionsTest.php index be463ab5d0950fc4c54ad943cf68773c7c8c7aa7..2c42bd00dbc3a2859ceb18df5fe973e6aa9b4f6e 100644 --- a/core/modules/system/lib/Drupal/system/Tests/System/ShutdownFunctionsTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/System/ShutdownFunctionsTest.php @@ -29,14 +29,6 @@ public static function getInfo() { ); } - protected function tearDown() { - // This test intentionally throws an exception in a PHP shutdown function. - // Prevent it from being interpreted as an actual test failure. - // Not using File API; a potential error must trigger a PHP warning. - unlink(DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log'); - parent::tearDown(); - } - /** * Test shutdown functions. */ diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 5e32a6b060efc1eb0db38caa691ad147cd91cdcb..4f90a5768d54ee82b3e5042c7a0770f80d3f0335 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -326,8 +326,9 @@ function system_requirements($phase) { } else { // If we are installing Drupal, the settings.php file might not exist yet - // in the intended site directory, so don't require it. - $directories[] = conf_path(FALSE) . '/files'; + // in the intended conf_path() directory, so don't require it. The + // conf_path() cache must also be reset in this case. + $directories[] = conf_path(FALSE, TRUE) . '/files'; } if (!empty($GLOBALS['config']['system.file']['path']['private'])) { $directories[] = $GLOBALS['config']['system.file']['path']['private']; diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh index 5594dd532504aaab69621f7e06266a1c1217aa68..9b1a7bb1105e9eb84213d49240cca9a40a3b3335 100755 --- a/core/scripts/run-tests.sh +++ b/core/scripts/run-tests.sh @@ -7,6 +7,7 @@ require_once __DIR__ . '/../vendor/autoload.php'; use Drupal\Component\Utility\Timer; +use Drupal\Core\StreamWrapper\PublicStream; const SIMPLETEST_SCRIPT_COLOR_PASS = 32; // Green. const SIMPLETEST_SCRIPT_COLOR_FAIL = 31; // Red. @@ -415,7 +416,8 @@ function simpletest_script_execute_batch($test_classes) { echo 'FATAL ' . $child['class'] . ': test runner returned a non-zero error code (' . $status['exitcode'] . ').' . "\n"; if ($args['die-on-fail']) { list($db_prefix, ) = simpletest_last_test_get($child['test_id']); - $test_directory = 'sites/simpletest/' . substr($db_prefix, 10); + $public_files = PublicStream::basePath(); + $test_directory = $public_files . '/simpletest/' . substr($db_prefix, 10); echo 'Simpletest database and files kept and test exited immediately on fail so should be reproducible if you change settings.php to use the database prefix '. $db_prefix . ' and config directories in '. $test_directory . "\n"; $args['keep-results'] = TRUE; // Exit repeat loop immediately. @@ -581,9 +583,10 @@ function simpletest_script_cleanup($test_id, $test_class, $exitcode) { // Read the log file in case any fatal errors caused the test to crash. simpletest_log_read($test_id, $db_prefix, $test_class); - // Check whether a test site directory was setup already. - // @see \Drupal\simpletest\TestBase::prepareEnvironment() - $test_directory = DRUPAL_ROOT . '/sites/simpletest/' . substr($db_prefix, 10); + // Check whether a test file directory was setup already. + // @see prepareEnvironment() + $public_files = PublicStream::basePath(); + $test_directory = $public_files . '/simpletest/' . substr($db_prefix, 10); if (is_dir($test_directory)) { // Output the error_log. if (is_file($test_directory . '/error.log')) { @@ -592,7 +595,8 @@ function simpletest_script_cleanup($test_id, $test_class, $exitcode) { $messages[] = $errors; } } - // Delete the test site directory. + + // Delete the test files directory. // simpletest_clean_temporary_directories() cannot be used here, since it // would also delete file directories of other tests that are potentially // running concurrently.