diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index be8c695..ea553b2 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) {
-  $conf_path = &drupal_static(__FUNCTION__, '');
+  static $conf_path;
 
-  if ($conf_path && !$reset) {
+  if (isset($conf_path) && !$reset) {
     return $conf_path;
   }
 
   // Check for a simpletest override.
-  if ($simpletest_conf_path = _drupal_simpletest_conf_path()) {
-    $conf_path = $simpletest_conf_path;
+  if ($test_prefix = drupal_valid_test_ua()) {
+    $conf_path = 'sites/simpletest/' . substr($test_prefix, 10);
     return $conf_path;
   }
 
@@ -263,50 +263,6 @@ function conf_path($require_settings = TRUE, $reset = FALSE) {
 }
 
 /**
- * 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.
  *
  * Finds a matching configuration directory file by stripping the website's
@@ -534,19 +490,33 @@ 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 $databases, $cookie_domain, $conf, $db_prefix, $drupal_hash_salt, $base_secure_url, $base_insecure_url, $config_directories;
+  global $base_url, $databases, $cookie_domain, $conf, $drupal_hash_salt, $config_directories;
   $conf = array();
+  $settings = array();
 
   // Make conf_path() available as local variable in settings.php.
   $conf_path = conf_path();
   if (is_readable(DRUPAL_ROOT . '/' . $conf_path . '/settings.php')) {
-    include_once DRUPAL_ROOT . '/' . $conf_path . '/settings.php';
+    require DRUPAL_ROOT . '/' . $conf_path . '/settings.php';
   }
 
-  new Settings(isset($settings) ? $settings : array());
+  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';
 
   if (isset($base_url)) {
@@ -1789,11 +1759,27 @@ 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();
-
-  // Make sure we are using the test database prefix in child Drupal sites.
-  _drupal_initialize_db_test_prefix();
+  _drupal_request_initialize();
 
   // Activate the class loader.
   drupal_classloader();
@@ -1882,39 +1868,6 @@ 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.
  *
  * The current phase is the one most recently completed by drupal_bootstrap().
@@ -2028,10 +1981,9 @@ 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. Passed in by tests that use
- *   the test runner from within a test.
+ *   Internal use only. A new prefix to be stored.
  *
- * @return
+ * @return string|FALSE
  *   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.
@@ -2045,82 +1997,67 @@ 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;
-    // 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__);
+    // 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__);
     $time_diff = REQUEST_TIME - $time;
-    // 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('+' => '-', '/' => '_', '=' => ''));
+    $test_hmac = Crypt::hmacBase64($check_string, $key);
     // 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 $conf, the global $config_directories, the return
- * value of conf_path(), and settings().
- *
- * @param string $test_prefix
- *   The simpletest prefix.
- */
-function _drupal_load_test_overrides($test_prefix) {
-  global $conf, $config_directories;
-
-  // 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 $conf, $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;
-
-  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__);
+  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'.");
+      }
+      $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__);
   }
   // 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 b97cc5e..d80087d 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -3066,14 +3066,6 @@ 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 1e6de38..dfb967f 100644
--- a/core/includes/errors.inc
+++ b/core/includes/errors.inc
@@ -133,8 +133,7 @@ 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.
-  $test_info = &$GLOBALS['drupal_test_info'];
-  if (!empty($test_info['in_child_site']) && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) {
+  if (DRUPAL_TEST_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 0a57f16..d529511 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -276,19 +276,18 @@ 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. 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')) {
+  // 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()) {
     header($request->server->get('SERVER_PROTOCOL') . ' 403 Forbidden');
     exit;
   }
 
+  drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION);
+
   // If we have a language selected and it is not yet saved in the system (eg.
   // pre-database data screens we are unable to persistently store the default
   // language), we should set language_default so the proper language is used to
@@ -1066,7 +1065,6 @@ 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)) {
@@ -1089,7 +1087,6 @@ 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();
@@ -1151,12 +1148,6 @@ 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();
@@ -1223,33 +1214,14 @@ function install_settings_form_submit($form, &$form_state) {
   // Update global settings array and save.
   $settings = array();
   $database = $form_state['storage']['database'];
-  // 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,
-    );
-  }
+  $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(
@@ -2367,7 +2339,7 @@ function install_check_requirements($install_state) {
   if (!$install_state['settings_verified']) {
     $readable = FALSE;
     $writable = FALSE;
-    $conf_path = './' . conf_path(FALSE, TRUE);
+    $conf_path = './' . conf_path(FALSE);
     $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 06a89a8..9455b90 100644
--- a/core/lib/Drupal/Core/Controller/ExceptionController.php
+++ b/core/lib/Drupal/Core/Controller/ExceptionController.php
@@ -270,8 +270,7 @@ 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.
-    $test_info = &$GLOBALS['drupal_test_info'];
-    if (!empty($test_info['in_child_site']) && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) {
+    if (DRUPAL_TEST_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 633edc6..97eab54 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, TRUE) . '/files/.ht.sqlite';
+    $default_database = conf_path(FALSE) . '/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 46a7fee..e80d747 100644
--- a/core/lib/Drupal/Core/DrupalKernel.php
+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -335,20 +335,13 @@ public function updateModules(array $module_list, array $module_filenames = arra
   }
 
   /**
-   * Returns the classname based on environment and testing prefix.
+   * Returns the classname based on environment.
    *
    * @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 6f5bfd1..a8de4a8 100644
--- a/core/lib/Drupal/Core/Http/Plugin/SimpletestHttpRequestSubscriber.php
+++ b/core/lib/Drupal/Core/Http/Plugin/SimpletestHttpRequestSubscriber.php
@@ -34,9 +34,8 @@ 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.
-    $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']));
+    if ($test_prefix = drupal_valid_test_ua()) {
+      $event['request']->setHeader('User-Agent', drupal_generate_test_ua($test_prefix));
     }
   }
 }
diff --git a/core/lib/Drupal/Core/StreamWrapper/PublicStream.php b/core/lib/Drupal/Core/StreamWrapper/PublicStream.php
index 90f17b3..760a1f8 100644
--- a/core/lib/Drupal/Core/StreamWrapper/PublicStream.php
+++ b/core/lib/Drupal/Core/StreamWrapper/PublicStream.php
@@ -38,13 +38,6 @@ 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 ff1c584..2461165 100644
--- a/core/modules/field/lib/Drupal/field/Tests/FieldImportCreateTest.php
+++ b/core/modules/field/lib/Drupal/field/Tests/FieldImportCreateTest.php
@@ -95,11 +95,12 @@ function testImportCreate() {
 
     // Add the new files to the staging directory.
     $src_dir = drupal_get_path('module', 'field_test_config') . '/staging';
-    $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"));
+    $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"));
 
     // 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 b427d3f..d230bc5 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 'sites/default/files';
+    return conf_path() . '/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 4836f09..cbea40f 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 'sites/default/files';
+    return conf_path() . '/files';
   }
 
   /**
diff --git a/core/modules/language/lib/Drupal/language/Tests/LanguageNegotiationInfoTest.php b/core/modules/language/lib/Drupal/language/Tests/LanguageNegotiationInfoTest.php
index 58cbd72..70d297c 100644
--- a/core/modules/language/lib/Drupal/language/Tests/LanguageNegotiationInfoTest.php
+++ b/core/modules/language/lib/Drupal/language/Tests/LanguageNegotiationInfoTest.php
@@ -24,13 +24,6 @@ class LanguageNegotiationInfoTest extends WebTestBase {
   public static $modules = array('language');
 
   /**
-   * The language manager.
-   *
-   * @var \Drupal\language\ConfigurableLanguageManagerInterface
-   */
-  protected $languageManager;
-
-  /**
    * {@inheritdoc}
    */
   public static function getInfo() {
@@ -46,31 +39,60 @@ 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() {
-    // 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();
+    $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();
 
     // 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
@@ -87,30 +109,34 @@ function testInfoAlterations() {
     );
     $this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings'));
 
-    // 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();
+    // 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');
     $this->assertFalse(isset($negotiation[$interface_method_id]), 'Interface language negotiation method removed from the stored settings.');
-    $this->assertNoFieldByXPath("//input[@name=\"$form_field\"]", NULL, 'Interface language negotiation method unavailable.');
+
+    $this->drupalGet('admin/config/regional/language/detection');
+    $this->assertNoFieldByName($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->assertFieldByXPath("//input[@name=\"$form_field\"]", NULL, format_string('Type-specific test language negotiation method available for %type.', array('%type' => $type)));
+        $this->assertFieldByName($form_field, NULL, format_string('Type-specific test language negotiation method available for %type.', array('%type' => $type)));
       }
       else {
-        $this->assertNoFieldByXPath("//input[@name=\"$form_field\"]", NULL, format_string('Type-specific test language negotiation method unavailable for %type.', array('%type' => $type)));
+        $this->assertNoFieldByName($form_field, NULL, format_string('Type-specific test language negotiation method unavailable for %type.', array('%type' => $type)));
       }
     }
 
     // Check language negotiation results.
     $this->drupalGet('');
-    $last = \Drupal::state()->get('language_test.language_negotiation_last');
-    foreach ($this->languageManager->getDefinedLanguageTypes() as $type) {
+    $last = $this->container->get('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)));
@@ -118,10 +144,11 @@ function testInfoAlterations() {
 
     // Uninstall language_test and check that everything is set back to the
     // original status.
-    $this->languageNegotiationUpdate('uninstall');
+    $this->container->get('module_handler')->uninstall(array('language_test'));
+    $this->rebuildContainer();
 
     // 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)));
     }
 
@@ -131,7 +158,7 @@ function testInfoAlterations() {
 
     // Check that unavailable language negotiation methods are not present in
     // the negotiation settings.
-    $negotiation = \Drupal::config('language.types')->get('negotiation.' . $type . '.enabled') ?: array();
+    $negotiation = $this->container->get('config.factory')->get('language.types')->get('negotiation.' . $type . '.enabled');
     $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.
@@ -140,40 +167,13 @@ function testInfoAlterations() {
   }
 
   /**
-   * 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 = \Drupal::config('language.types')->get('negotiation.' . $type . '.enabled') ?: array();
+        $negotiation = $this->container->get('config.factory')->get('language.types')->get('negotiation.' . $type . '.enabled');
         $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 c6d4b7a..1d59ca1 100644
--- a/core/modules/node/lib/Drupal/node/Tests/Config/NodeImportCreateTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/Config/NodeImportCreateTest.php
@@ -71,7 +71,8 @@ public function testImportCreate() {
     $this->copyConfig($active, $staging);
     // Manually add new node type.
     $src_dir = drupal_get_path('module', 'node_test_config') . '/staging';
-    $this->assertTrue(file_unmanaged_copy("$src_dir/$node_type_config_name.yml", "public://config_staging/$node_type_config_name.yml"));
+    $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"));
 
     // 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 92b161f..6ee194c 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php
@@ -58,6 +58,13 @@
   private $themeData;
 
   /**
+   * The configuration directories for this test run.
+   *
+   * @var array
+   */
+  protected $configDirectories = array();
+
+  /**
    * A KeyValueMemoryFactory instance to use when building the container.
    *
    * @var \Drupal\Core\KeyValueStore\KeyValueMemoryFactory.
@@ -94,6 +101,27 @@ 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.
    */
   protected function setUp() {
@@ -101,6 +129,9 @@ 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.
@@ -157,7 +188,9 @@ protected function setUp() {
   }
 
   protected function tearDown() {
-    $this->kernel->shutdown();
+    if ($this->kernel instanceof DrupalKernel) {
+      $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 b9eb05a..13a353d 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php
@@ -37,6 +37,13 @@
   protected $testId;
 
   /**
+   * The site directory of this test run.
+   *
+   * @var string
+   */
+  protected $siteDirectory = NULL;
+
+  /**
    * The database prefix of this test run.
    *
    * @var string
@@ -152,6 +159,13 @@
   public $dieOnFail = FALSE;
 
   /**
+   * The DrupalKernel instance used in the test.
+   *
+   * @var \Drupal\Core\DrupalKernel
+   */
+  protected $kernel;
+
+  /**
    * The dependency injection container used in the test.
    *
    * @var \Symfony\Component\DependencyInjection\ContainerInterface
@@ -606,7 +620,20 @@ 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.
@@ -851,7 +878,9 @@ public function run(array $methods = array()) {
    * @see drupal_valid_test_ua()
    */
   private function prepareDatabasePrefix() {
-    $this->databasePrefix = 'simpletest' . mt_rand(1000, 1000000);
+    $suffix = mt_rand(1000, 1000000);
+    $this->siteDirectory = 'sites/simpletest/' . $suffix;
+    $this->databasePrefix = 'simpletest' . $suffix;
 
     // 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
@@ -879,19 +908,9 @@ private function changeDatabasePrefix() {
     $connection_info = Database::getConnectionInfo('default');
     Database::renameConnection('default', 'simpletest_original_default');
     foreach ($connection_info as $target => $value) {
-      $connection_info[$target]['prefix'] = array(
-        'default' => $value['prefix']['default'] . $this->databasePrefix,
-      );
+      $connection_info[$target]['prefix'] = $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'];
   }
 
   /**
@@ -932,10 +951,9 @@ private function prepareEnvironment() {
     $language_interface = language(Language::TYPE_INTERFACE);
 
     // When running the test runner within a test, back up the original database
-    // prefix and re-set the new/nested prefix in drupal_valid_test_ua().
-    if (drupal_valid_test_ua()) {
+    // prefix.
+    if (DRUPAL_TEST_IN_CHILD_SITE) {
       $this->originalPrefix = drupal_valid_test_ua();
-      drupal_valid_test_ua($this->databasePrefix);
     }
 
     // Backup current in-memory configuration.
@@ -975,21 +993,15 @@ private function prepareEnvironment() {
 
     // Create test directory ahead of installation so fatal errors and debug
     // information can be logged during installation process.
-    // 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;
+    file_prepare_directory($this->siteDirectory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
 
-    // Create and set new configuration directories.
-    $this->prepareConfigDirectories();
+    // 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';
+
+    $this->generatedTestFiles = FALSE;
 
     // Unregister all custom stream wrappers of the parent site.
     // Availability of Drupal stream wrappers varies by test base class:
@@ -1034,51 +1046,29 @@ 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', $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;
+    ini_set('error_log', DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log');
 
     // 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();
 
     // Reset all variables to perform tests in a clean environment.
     $conf = array();
 
-    drupal_set_time_limit($this->timeLimit);
-  }
+    // 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);
 
-  /**
-   * 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;
-    }
+    drupal_set_time_limit($this->timeLimit);
   }
 
   /**
@@ -1097,11 +1087,11 @@ protected function prepareConfigDirectories() {
    *   tests can invoke this workaround when requiring services from newly
    *   enabled modules to be immediately available in the same request.
    */
-  protected function rebuildContainer() {
+  protected function rebuildContainer($environment = 'testing') {
     // Preserve the request object after the container rebuild.
     $request = \Drupal::request();
 
-    $this->kernel = new DrupalKernel('testing', drupal_classloader(), FALSE);
+    $this->kernel = new DrupalKernel($environment, 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.
@@ -1163,8 +1153,12 @@ private function restoreEnvironment() {
       }
     }
 
-    // Delete temporary files directory.
-    file_unmanaged_delete_recursive($this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10), array($this, 'filePreDeleteCallback'));
+    // 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'));
 
     // Restore original database connection.
     Database::removeConnection('default');
@@ -1193,19 +1187,13 @@ 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 42b786a..8981519 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/Tests/BrokenSetUpTest.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/Tests/BrokenSetUpTest.php
@@ -28,6 +28,13 @@ 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',
@@ -38,15 +45,20 @@ public static function getInfo() {
 
   function setUp() {
     // If the test is being run from the main site, set up normally.
-    if (!drupal_valid_test_ua()) {
+    if (!$this->isInChildSite()) {
       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 {
-      if (file_get_contents($this->originalFileDirectory . '/simpletest/trigger') === 'setup') {
+      $this->sharedTriggerFile = $this->originalFileDirectory . '/trigger';
+
+      if (file_get_contents($this->sharedTriggerFile) === 'setup') {
         throw new \Exception('Broken setup');
       }
       $this->pass('The setUp() method has run.');
@@ -55,13 +67,13 @@ function setUp() {
 
   function tearDown() {
     // If the test is being run from the main site, tear down normally.
-    if (!drupal_valid_test_ua()) {
-      unlink($this->originalFileDirectory . '/simpletest/trigger');
+    if (!$this->isInChildSite()) {
+      unlink($this->sharedTriggerFile);
       parent::tearDown();
     }
     // If the test is being run from within simpletest, output a message.
     else {
-      if (file_get_contents($this->originalFileDirectory . '/simpletest/trigger') === 'teardown') {
+      if (file_get_contents($this->sharedTriggerFile) === 'teardown') {
         throw new \Exception('Broken teardown');
       }
       $this->pass('The tearDown() method has run.');
@@ -74,9 +86,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 (!drupal_valid_test_ua()) {
+    if (!$this->isInChildSite()) {
       // Verify that a broken setUp() method is caught.
-      file_put_contents($this->originalFileDirectory . '/simpletest/trigger', 'setup');
+      file_put_contents($this->sharedTriggerFile, 'setup');
       $edit['Drupal\simpletest\Tests\BrokenSetUpTest'] = TRUE;
       $this->drupalPostForm('admin/config/development/testing', $edit, t('Run tests'));
       $this->assertRaw('Broken setup');
@@ -87,7 +99,7 @@ function testMethod() {
       $this->assertNoRaw('The tearDown() method has run.');
 
       // Verify that a broken tearDown() method is caught.
-      file_put_contents($this->originalFileDirectory . '/simpletest/trigger', 'teardown');
+      file_put_contents($this->sharedTriggerFile, 'teardown');
       $edit['Drupal\simpletest\Tests\BrokenSetUpTest'] = TRUE;
       $this->drupalPostForm('admin/config/development/testing', $edit, t('Run tests'));
       $this->assertNoRaw('Broken setup');
@@ -98,7 +110,7 @@ function testMethod() {
       $this->assertNoRaw('The tearDown() method has run.');
 
       // Verify that a broken test method is caught.
-      file_put_contents($this->originalFileDirectory . '/simpletest/trigger', 'test');
+      file_put_contents($this->sharedTriggerFile, 'test');
       $edit['Drupal\simpletest\Tests\BrokenSetUpTest'] = TRUE;
       $this->drupalPostForm('admin/config/development/testing', $edit, t('Run tests'));
       $this->assertNoRaw('Broken setup');
@@ -110,7 +122,7 @@ function testMethod() {
     }
     // If the test is being run from within simpletest, output a message.
     else {
-      if (file_get_contents($this->originalFileDirectory . '/simpletest/trigger') === 'test') {
+      if (file_get_contents($this->sharedTriggerFile) === '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 61d6401..24b4348 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 (drupal_valid_test_ua()) {
+    if ($this->isInChildSite()) {
       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 (!drupal_valid_test_ua()) {
+    if (!$this->isInChildSite()) {
       // 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 2a19a5c1..5578111 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->inCURL()) {
+    if (!$this->isInChildSite()) {
       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->inCURL()) {
+    if (!$this->isInChildSite()) {
       // 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,10 +91,11 @@ function testInternalBrowser() {
       $headers = $this->drupalGetHeaders(TRUE);
       $this->assertEqual(count($headers), 2, 'Simpletest stopped following redirects after the first one.');
 
-      // Remove the Simpletest settings.php so we can test the protection
+      // Remove the Simpletest private key file so we can test the protection
       // against requests that forge a valid testing user agent to gain access
       // to the installer.
-      drupal_unlink($this->public_files_directory . '/settings.php');
+      // @see drupal_valid_test_ua()
+      drupal_unlink($this->siteDirectory . '/.htkey');
       global $base_url;
       $this->drupalGet(url($base_url . '/core/install.php', array('external' => TRUE, 'absolute' => TRUE)));
       $this->assertResponse(403, 'Cannot access install.php.');
@@ -105,7 +106,7 @@ function testInternalBrowser() {
    * Test validation of the User-Agent header we use to perform test requests.
    */
   function testUserAgentValidation() {
-    if (!$this->inCURL()) {
+    if (!$this->isInChildSite()) {
       global $base_url;
       $system_path = $base_url . '/' . drupal_get_path('module', 'system');
       $HTTP_path = $system_path .'/tests/http.php?q=node';
@@ -147,7 +148,7 @@ function testWebTestRunner() {
     $this->valid_permission = 'access content';
     $this->invalid_permission = 'invalid permission';
 
-    if ($this->inCURL()) {
+    if ($this->isInChildSite()) {
       // Only run following code if this test is running itself through a CURL
       // request.
       $this->stubTest();
@@ -335,10 +336,4 @@ 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 86a7254..643f652 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/UnitTestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/UnitTestBase.php
@@ -20,11 +20,6 @@
 abstract class UnitTestBase extends TestBase {
 
   /**
-   * @var array
-   */
- protected $configDirectories;
-
-  /**
    * Constructor for UnitTestBase.
    */
   function __construct($test_id = NULL) {
@@ -41,6 +36,7 @@ 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 b284941..e003caf 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
@@ -730,56 +730,78 @@ protected function setUp() {
     // Reset the static batch to remove Simpletest's batch operations.
     $batch = &batch_get();
     $batch = array();
-    $variable_groups = array(
-      'system.file' => array(
-        'path.private' =>  $this->private_files_directory,
-        'path.temporary' =>  $this->temp_files_directory,
-      ),
-      'locale.settings' =>  array(
-        'translation.path' => $this->translation_files_directory,
-      ),
+
+    // 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.
+    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,
     );
-    foreach ($variable_groups as $config_base => $variables) {
-      foreach ($variables as $name => $value) {
-        NestedArray::setValue($GLOBALS['conf'], array_merge(array($config_base), explode('.', $name)), $value);
-      }
-    }
-    $this->settingsSet('file_public_path', $this->public_files_directory);
+    // 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();
+
     // 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);
 
-    // 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);
+    // 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.
+    // Use chmod() without a Drupal wrapper, so potential errors are visible.
+    // TestBase::restoreEnvironment() will delete the entire site directory.
+    chmod(DRUPAL_ROOT . '/' . $this->siteDirectory, 0777);
 
-    $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();
@@ -796,22 +818,18 @@ protected function setUp() {
       $this->rebuildContainer();
     }
 
-    // Reset/rebuild all data structures after enabling the modules.
+    // 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.
     $this->resetAll();
 
-    // Now make sure that the file path configurations are saved. This is done
-    // after we install the modules to override default values.
-    foreach ($variable_groups as $config_base => $variables) {
-      $config = \Drupal::config($config_base);
-      foreach ($variables as $name => $value) {
-        $config->set($name, $value);
-      }
-      $config->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();
@@ -868,40 +886,28 @@ protected function installParameters() {
   }
 
   /**
-   * Writes a test-specific settings.php file for the child site.
-   *
-   * The child site loads this after the parent site's settings.php, so settings
-   * here override those.
+   * Rewrites the settings.php file of the test site.
    *
-   * @param $settings An array of settings to write out, in the format expected
-   *   by drupal_rewrite_settings().
+   * @param array $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($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;
-    }
-
+  protected function writeSettings(array $settings) {
     include_once DRUPAL_ROOT . '/core/includes/install.inc';
-    $filename = $this->public_files_directory . '/settings.php';
-    file_put_contents($filename, "<?php\n");
+    $filename = $this->siteDirectory . '/settings.php';
+    // system_requirements() removes write permissions from settings.php
+    // whenever it is invoked.
+    chmod($filename, 0666);
     drupal_rewrite_settings($settings, $filename);
-
-    // Restore the original globals.
-    foreach ($original_globals as $variable_name => $value) {
-      $GLOBALS[$variable_name] = $value;
-    }
   }
 
   /**
-   * Sets custom translations to the settings object and queues them to writing.
+   * Queues custom translations to be written to settings.php.
    *
-   * In order for those custom translations to persist (being written in test
-   * site's settings.php) make sure to also call self::writeCustomTranslations()
+   * Use WebTestBase::writeCustomTranslations() to apply and write the queued
+   * translations.
    *
    * @param string $langcode
    *   The langcode to add translations for.
@@ -914,32 +920,56 @@ protected function writeSettings($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) {
-    $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,
-        );
+    // 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;
+        }
       }
     }
   }
 
   /**
-   * Writes custom translations to test site's settings.php.
+   * Writes custom translations to the test site's settings.php.
+   *
+   * Use TestBase::addCustomTranslations() to queue custom translations before
+   * calling this method.
    */
   protected function writeCustomTranslations() {
-    $this->writeSettings(array('settings' => $this->customTranslations));
-    $this->customTranslations = array();
+    $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);
+    }
   }
 
   /**
    * Overrides \Drupal\simpletest\TestBase::rebuildContainer().
    */
-  protected function rebuildContainer() {
-    parent::rebuildContainer();
+  protected function rebuildContainer($environment = 'prod') {
+    parent::rebuildContainer($environment);
     // Make sure the url generator has a request object, otherwise calls to
     // $this->drupalGet() will fail.
     $this->prepareRequestForGenerator();
@@ -979,8 +1009,8 @@ protected function refreshVariables() {
     // Clear the tag cache.
     drupal_static_reset('Drupal\Core\Cache\CacheBackendInterface::tagCache');
 
-    \Drupal::service('config.factory')->reset();
-    \Drupal::state()->resetCache();
+    $this->container->get('config.factory')->reset();
+    $this->container->get('state')->resetCache();
   }
 
   /**
@@ -1244,6 +1274,21 @@ 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.
    *
    * @return
@@ -1286,10 +1331,18 @@ 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();
@@ -1809,6 +1862,28 @@ protected function serializePostValues($post = array()) {
   }
 
   /**
+   * 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.
    */
   protected function cronRun() {
diff --git a/core/modules/simpletest/simpletest.api.php b/core/modules/simpletest/simpletest.api.php
index e11c651..dad9a39 100644
--- a/core/modules/simpletest/simpletest.api.php
+++ b/core/modules/simpletest/simpletest.api.php
@@ -6,18 +6,6 @@
  */
 
 /**
- * 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 a863581..0f352fe 100644
--- a/core/modules/simpletest/simpletest.install
+++ b/core/modules/simpletest/simpletest.install
@@ -58,6 +58,28 @@ 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;
 }
 
@@ -157,9 +179,13 @@ function simpletest_schema() {
  * Implements hook_uninstall().
  */
 function simpletest_uninstall() {
-  simpletest_clean_database();
-
-  // Remove generated files.
+  // 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.
   file_unmanaged_delete_recursive('public://simpletest');
 }
 
diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module
index c6b53bf..82db60e 100644
--- a/core/modules/simpletest/simpletest.module
+++ b/core/modules/simpletest/simpletest.module
@@ -383,18 +383,16 @@ function simpletest_last_test_get($test_id) {
  *
  * @param $test_id
  *   The test ID to which the log relates.
- * @param $prefix
+ * @param $database_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, $prefix, $test_class, $during_test = FALSE) {
-  $log = 'public://' . ($during_test ? '' : '/simpletest/' . substr($prefix, 10)) . '/error.log';
+function simpletest_log_read($test_id, $database_prefix, $test_class) {
+  $log = DRUPAL_ROOT . '/sites/simpletest/' . substr($database_prefix, 10) . '/error.log';
   $found = FALSE;
   if (file_exists($log)) {
     foreach (file($log) as $line) {
@@ -648,11 +646,11 @@ function simpletest_clean_database() {
  */
 function simpletest_clean_temporary_directories() {
   $count = 0;
-  if (is_dir('public://simpletest')) {
-    $files = scandir('public://simpletest');
+  if (is_dir(DRUPAL_ROOT . '/sites/simpletest')) {
+    $files = scandir(DRUPAL_ROOT . '/sites/simpletest');
     foreach ($files as $file) {
-      $path = 'public://simpletest/' . $file;
-      if (is_dir($path) && (is_numeric($file) || strpos($file, 'config_simpletest') !== FALSE)) {
+      if ($file[0] != '.') {
+        $path = DRUPAL_ROOT . '/sites/simpletest/' . $file;
         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 d75db82..1e08bd7 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Ajax/FormValuesTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Ajax/FormValuesTest.php
@@ -53,6 +53,8 @@ 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(
@@ -61,5 +63,7 @@ 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.
+    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 ae61ae5..76d0fde 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\UnitTestBase;
+use Drupal\simpletest\DrupalUnitTestBase;
 
 /**
  * Tests compilation of the DIC.
  */
-class DrupalKernelTest extends UnitTestBase {
+class DrupalKernelTest extends DrupalUnitTestBase {
 
   public static function getInfo() {
     return array(
@@ -26,7 +26,13 @@ public static function getInfo() {
   }
 
   function setUp() {
-    parent::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();
+
     $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 f87fdc5..2726a22 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Installer/InstallerTranslationTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Installer/InstallerTranslationTest.php
@@ -7,7 +7,6 @@
 
 namespace Drupal\system\Tests\Installer;
 
-use Drupal\Component\Utility\NestedArray;
 use Drupal\system\Tests\InstallerTest;
 
 /**
@@ -16,11 +15,11 @@
 class InstallerTranslationTest extends InstallerTest {
 
   /**
-   * Whether the installer has completed.
+   * Overrides the language code in which to install Drupal.
    *
-   * @var bool
+   * @var string
    */
-  protected $isInstalled = FALSE;
+  protected $langcode = 'de';
 
   public static function getInfo() {
     return array(
@@ -30,92 +29,34 @@ public static function getInfo() {
     );
   }
 
-  protected function setUp() {
-    $this->isInstalled = FALSE;
-
-    $variable_groups = array(
-      'system.file' => array(
-        'path.private' =>  $this->private_files_directory,
-        'path.temporary' => $this->temp_files_directory,
-      ),
-      'locale.settings' => array(
-        'translation.path' => $this->translation_files_directory,
-      ),
-    );
-    foreach ($variable_groups as $config_base => $variables) {
-      foreach ($variables as $name => $value) {
-        NestedArray::setValue($GLOBALS['conf'], array_merge(array($config_base), explode('.', $name)), $value);
-      }
-    }
-    $settings['conf_path'] = (object) array(
-      'value' => $this->public_files_directory,
-      'required' => TRUE,
-    );
-    $settings['config_directories'] = (object) array(
-      'value' => array(),
-      'required' => TRUE,
-    );
-    $this->writeSettings($settings);
-
-    // Submit the installer with German language.
-    $this->drupalPostForm($GLOBALS['base_url'] . '/core/install.php', array('langcode' => 'de'), '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'));
-
-    // Submit the standard profile installation.
-    $this->drupalPostForm(NULL, array('profile' => 'standard'), $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();
-
-    foreach ($variable_groups as $config_base => $variables) {
-      $config = \Drupal::config($config_base);
-      foreach ($variables as $name => $value) {
-        $config->set($name, $value);
-      }
-      $config->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');
-    }
+  /**
+   * 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;
+  }
 
-    $this->isInstalled = TRUE;
+  /**
+   * 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();
   }
 
 }
diff --git a/core/modules/system/lib/Drupal/system/Tests/InstallerTest.php b/core/modules/system/lib/Drupal/system/Tests/InstallerTest.php
index d4f1c5c..4fce131 100644
--- a/core/modules/system/lib/Drupal/system/Tests/InstallerTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/InstallerTest.php
@@ -8,14 +8,61 @@
 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.
    *
    * @var bool
@@ -30,52 +77,73 @@ public static function getInfo() {
     );
   }
 
+  /**
+   * Overrides WebTestBase::setUp().
+   */
   protected function setUp() {
     $this->isInstalled = FALSE;
 
-    $variable_groups = array(
-      'system.file' => array(
-        'path.private' =>  $this->private_files_directory,
-        'path.temporary' =>  $this->temp_files_directory,
-      ),
-      'locale.settings' =>  array(
-        'translation.path' => $this->translation_files_directory,
-      ),
-    );
-    foreach ($variable_groups as $config_base => $variables) {
-      foreach ($variables as $name => $value) {
-        NestedArray::setValue($GLOBALS['conf'], array_merge(array($config_base), explode('.', $name)), $value);
-      }
+    // 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)) {
+      copy(DRUPAL_ROOT . '/sites/default/default.settings.php', DRUPAL_ROOT . '/' . $this->siteDirectory . '/settings.php');
+      $this->writeSettings($settings);
     }
-    $settings['conf_path'] = (object) array(
-      'value' => $this->public_files_directory,
-      'required' => TRUE,
-    );
-    $settings['config_directories'] = (object) array(
-      'value' => array(),
-      '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;
-    }
-    $this->rebuildContainer();
 
-    foreach ($variable_groups as $config_base => $variables) {
-      $config = \Drupal::config($config_base);
-      foreach ($variables as $name => $value) {
-        $config->set($name, $value);
-      }
-      $config->save();
+    // 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;
     }
 
-    // Use the test mail class instead of the default mail handler class.
-    \Drupal::config('system.mail')->set('interface.default', 'Drupal\Core\Mail\TestMailCollector')->save();
+    // 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.
+    // Use chmod() without a Drupal wrapper, so potential errors are visible.
+    // WebTestBase::tearDown() will delete the entire test site directory.
+    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')
+      ->save();
 
     // When running from run-tests.sh we don't get an empty current path which
     // would indicate we're on the home page.
@@ -88,6 +156,49 @@ protected function setUp() {
   }
 
   /**
+   * 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}
    *
    * WebTestBase::refreshVariables() tries to operate on persistent storage,
@@ -100,34 +211,13 @@ 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->drupalGet('user');
+    $this->assertUrl('user/1');
     $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 7d20be9..ee37efa 100644
--- a/core/modules/system/lib/Drupal/system/Tests/System/ErrorHandlerTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/System/ErrorHandlerTest.php
@@ -94,6 +94,9 @@ 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',
@@ -121,6 +124,9 @@ 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.
+    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 96b2f8d..930be2c 100644
--- a/core/modules/system/lib/Drupal/system/Tests/System/ShutdownFunctionsTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/System/ShutdownFunctionsTest.php
@@ -29,6 +29,13 @@ 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.
+    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 25f75c7..931aafe 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -327,15 +327,14 @@ function system_requirements($phase) {
     }
     else {
       // If we are installing Drupal, the settings.php file might not exist yet
-      // 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';
+      // in the intended site directory, so don't require it.
+      $directories[] = conf_path(FALSE) . '/files';
     }
-    if (!empty($conf['system.file']['path.private'])) {
-      $directories[] = $conf['system.file']['path.private'];
+    if (!empty($conf['system.file']['path']['private'])) {
+      $directories[] = $conf['system.file']['path']['private'];
     }
-    if (!empty($conf['system.file']['path.temporary'])) {
-      $directories[] = $conf['system.file']['path.temporary'];
+    if (!empty($conf['system.file']['path']['temporary'])) {
+      $directories[] = $conf['system.file']['path']['temporary'];
     }
     else {
       // If the temporary directory is not overridden use an appropriate
diff --git a/core/modules/views_ui/lib/Drupal/views_ui/ViewListController.php b/core/modules/views_ui/lib/Drupal/views_ui/ViewListController.php
index 363a285..dd2775a 100644
--- a/core/modules/views_ui/lib/Drupal/views_ui/ViewListController.php
+++ b/core/modules/views_ui/lib/Drupal/views_ui/ViewListController.php
@@ -254,6 +254,10 @@ protected function getDisplayPaths(EntityInterface $view) {
     $executable = $view->getExecutable();
     $executable->initDisplay();
     foreach ($executable->displayHandlers as $display) {
+      // DisplayBag may return NULL if a plugin was not found.
+      if (!isset($display)) {
+        continue;
+      }
       if ($display->hasPath()) {
         $path = $display->getPath();
         if ($view->status() && strpos($path, '%') === FALSE) {
diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh
index d48c638..15c5077 100755
--- a/core/scripts/run-tests.sh
+++ b/core/scripts/run-tests.sh
@@ -7,7 +7,6 @@
 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.
@@ -416,8 +415,7 @@ 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']);
-            $public_files = PublicStream::basePath();
-            $test_directory = $public_files . '/simpletest/' . substr($db_prefix, 10);
+            $test_directory = 'sites/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.
@@ -586,10 +584,9 @@ 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 file directory was setup already.
-  // @see prepareEnvironment()
-  $public_files = PublicStream::basePath();
-  $test_directory = $public_files . '/simpletest/' . substr($db_prefix, 10);
+  // Check whether a test site directory was setup already.
+  // @see \Drupal\simpletest\TestBase::prepareEnvironment()
+  $test_directory = DRUPAL_ROOT . '/sites/simpletest/' . substr($db_prefix, 10);
   if (is_dir($test_directory)) {
     // Output the error_log.
     if (is_file($test_directory . '/error.log')) {
@@ -598,8 +595,7 @@ function simpletest_script_cleanup($test_id, $test_class, $exitcode) {
         $messages[] = $errors;
       }
     }
-
-    // Delete the test files directory.
+    // Delete the test site directory.
     // simpletest_clean_temporary_directories() cannot be used here, since it
     // would also delete file directories of other tests that are potentially
     // running concurrently.
